mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-06 02:48:13 +00:00
fix(tests): structural path matching for interactions compliance; xfail schema tests
The Google Interactions compliance suite fetches the live spec from
ai.google.dev, which recently restructured:
- the id placeholder is now named {interactionsId}, but the endpoint
existence matchers looked for the literal "{id}" and never matched,
even though LiteLLM implements GET and DELETE
/interactions/{interaction_id}. Match paths structurally (segment
shape + method) instead of by placeholder name.
- CreateModelInteractionParams was removed; the POST /interactions
request body is now an AgentInteraction | ModelInteraction oneOf.
The two schema-compliance tests still assert the old component, so
mark them xfail (non-strict) with that reason until the suite is
redesigned against the new component layout.
With this, the interactions compliance suite runs green against the
live spec again (11 passed, 2 xfailed).
This commit is contained in:
parent
684a1edd44
commit
8fe9eb9b2b
1 changed files with 52 additions and 37 deletions
|
|
@ -32,8 +32,7 @@ def _load_openapi_spec_dict() -> Dict[str, Any]:
|
|||
return response.json()
|
||||
except Exception as e: # pragma: no cover - defensive, env-dependent
|
||||
pytest.skip(
|
||||
f"Skipping Google Interactions OpenAPI compliance tests - "
|
||||
f"unable to load spec from {OPENAPI_SPEC_URL}: {e}"
|
||||
f"Skipping Google Interactions OpenAPI compliance tests - unable to load spec from {OPENAPI_SPEC_URL}: {e}"
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -44,6 +43,27 @@ def _declared_type_value(variant_schema: Dict[str, Any]) -> Any:
|
|||
return type_property.get("const") or (enum_values[0] if len(enum_values) == 1 else None)
|
||||
|
||||
|
||||
def _find_interaction_path(spec_paths: Dict[str, Any], method: str) -> str | None:
|
||||
"""
|
||||
Find the interactions path carrying `method`, regardless of what Google
|
||||
names the id placeholder (both `{id}` and `{interactionsId}` have appeared
|
||||
in this spec).
|
||||
"""
|
||||
method = method.lower()
|
||||
for path, methods in spec_paths.items():
|
||||
if "interactions" not in path or method not in {m.lower() for m in methods}:
|
||||
continue
|
||||
segments = [s for s in path.split("/") if s]
|
||||
if method == "post":
|
||||
if path.rstrip("/").endswith("/interactions"):
|
||||
return path # create
|
||||
if segments and segments[-1] == "cancel":
|
||||
return path # cancel
|
||||
elif segments and segments[-1].startswith("{") and segments[-1].endswith("}"):
|
||||
return path # get / delete by id
|
||||
return None
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def spec_dict() -> Dict[str, Any]:
|
||||
"""Load raw spec dict for manual validation."""
|
||||
|
|
@ -59,6 +79,15 @@ def openapi_spec(spec_dict: Dict[str, Any]) -> OpenAPI:
|
|||
class TestRequestCompliance:
|
||||
"""Tests that our request bodies match the OpenAPI spec."""
|
||||
|
||||
@pytest.mark.xfail(
|
||||
reason=(
|
||||
"Google restructured the live spec: CreateModelInteractionParams was "
|
||||
"replaced by an AgentInteraction | ModelInteraction request body oneOf, "
|
||||
"so the schema assertions below no longer match. The suite needs a "
|
||||
"redesign against the new component layout."
|
||||
),
|
||||
strict=False,
|
||||
)
|
||||
def test_create_model_interaction_request_schema(self, spec_dict):
|
||||
"""Verify CreateModelInteractionParams schema fields."""
|
||||
schema = spec_dict["components"]["schemas"]["CreateModelInteractionParams"]
|
||||
|
|
@ -86,6 +115,14 @@ class TestRequestCompliance:
|
|||
assert field in spec_properties, f"Field '{field}' not in OpenAPI spec"
|
||||
print(f"✓ Field '{field}' exists in spec")
|
||||
|
||||
@pytest.mark.xfail(
|
||||
reason=(
|
||||
"Google restructured the live spec: the input schema moved with the "
|
||||
"CreateModelInteractionParams removal (now AgentInteraction | "
|
||||
"ModelInteraction). See test_create_model_interaction_request_schema."
|
||||
),
|
||||
strict=False,
|
||||
)
|
||||
def test_input_types_match_spec(self, spec_dict):
|
||||
"""Verify input field supports string, Content, Content[], Turn[]."""
|
||||
schema = spec_dict["components"]["schemas"]["CreateModelInteractionParams"]
|
||||
|
|
@ -125,22 +162,18 @@ class TestRequestCompliance:
|
|||
|
||||
discriminator = content_schema.get("discriminator")
|
||||
if discriminator is not None:
|
||||
assert (
|
||||
discriminator.get("propertyName") == "type"
|
||||
), f"Content is discriminated on {discriminator.get('propertyName')!r}, not 'type'"
|
||||
assert discriminator.get("propertyName") == "type", (
|
||||
f"Content is discriminated on {discriminator.get('propertyName')!r}, not 'type'"
|
||||
)
|
||||
|
||||
variant_names = [
|
||||
option["$ref"].split("/")[-1]
|
||||
for option in content_schema.get("oneOf", [])
|
||||
if "$ref" in option
|
||||
option["$ref"].split("/")[-1] for option in content_schema.get("oneOf", []) if "$ref" in option
|
||||
]
|
||||
assert variant_names, f"Content is not a union of named variants: {content_schema}"
|
||||
|
||||
mapping = (discriminator or {}).get("mapping") or {}
|
||||
type_values = {
|
||||
variant: mapping_value
|
||||
for mapping_value, ref in mapping.items()
|
||||
for variant in [ref.split("/")[-1]]
|
||||
variant: mapping_value for mapping_value, ref in mapping.items() for variant in [ref.split("/")[-1]]
|
||||
} or {
|
||||
variant: _declared_type_value(spec_dict["components"]["schemas"].get(variant, {}))
|
||||
for variant in variant_names
|
||||
|
|
@ -191,7 +224,9 @@ class TestRequestCompliance:
|
|||
for option in spec_dict["components"]["schemas"]["Step"]["oneOf"]
|
||||
if "$ref" in option
|
||||
}
|
||||
assert {"UserInputStep", "ModelOutputStep"} <= step_variants, f"Step union is missing role steps: {step_variants}"
|
||||
assert {"UserInputStep", "ModelOutputStep"} <= step_variants, (
|
||||
f"Step union is missing role steps: {step_variants}"
|
||||
)
|
||||
|
||||
for step_name, type_value in [("UserInputStep", "user_input"), ("ModelOutputStep", "model_output")]:
|
||||
step_schema = spec_dict["components"]["schemas"][step_name]
|
||||
|
|
@ -261,9 +296,7 @@ class TestResponseCompliance:
|
|||
expected_fields = ["total_input_tokens", "total_output_tokens", "total_tokens"]
|
||||
|
||||
for field in expected_fields:
|
||||
assert (
|
||||
field in usage_schema["properties"]
|
||||
), f"Usage field '{field}' not in spec"
|
||||
assert field in usage_schema["properties"], f"Usage field '{field}' not in spec"
|
||||
print(f"✓ Usage field '{field}' exists")
|
||||
|
||||
|
||||
|
|
@ -282,9 +315,7 @@ class TestToolsCompliance:
|
|||
"""Verify FunctionDeclaration schema for function tools."""
|
||||
if "FunctionDeclaration" in spec_dict["components"]["schemas"]:
|
||||
func_schema = spec_dict["components"]["schemas"]["FunctionDeclaration"]
|
||||
assert "name" in func_schema.get(
|
||||
"properties", {}
|
||||
) or "name" in func_schema.get("required", [])
|
||||
assert "name" in func_schema.get("properties", {}) or "name" in func_schema.get("required", [])
|
||||
print("✓ FunctionDeclaration schema found")
|
||||
else:
|
||||
print("⚠ FunctionDeclaration schema not found (may be nested)")
|
||||
|
|
@ -309,27 +340,13 @@ class TestEndpointCompliance:
|
|||
|
||||
def test_get_endpoint_exists(self, spec_dict):
|
||||
"""Verify GET /interactions/{id} endpoint exists."""
|
||||
paths = spec_dict["paths"]
|
||||
|
||||
get_path = None
|
||||
for path, methods in paths.items():
|
||||
if "{id}" in path and "interactions" in path and "get" in methods:
|
||||
get_path = path
|
||||
break
|
||||
|
||||
get_path = _find_interaction_path(spec_dict["paths"], "get")
|
||||
assert get_path is not None, "GET /interactions/{id} endpoint not found"
|
||||
print(f"✓ Get endpoint: GET {get_path}")
|
||||
|
||||
def test_delete_endpoint_exists(self, spec_dict):
|
||||
"""Verify DELETE /interactions/{id} endpoint exists."""
|
||||
paths = spec_dict["paths"]
|
||||
|
||||
delete_path = None
|
||||
for path, methods in paths.items():
|
||||
if "{id}" in path and "interactions" in path and "delete" in methods:
|
||||
delete_path = path
|
||||
break
|
||||
|
||||
delete_path = _find_interaction_path(spec_dict["paths"], "delete")
|
||||
assert delete_path is not None, "DELETE /interactions/{id} endpoint not found"
|
||||
print(f"✓ Delete endpoint: DELETE {delete_path}")
|
||||
|
||||
|
|
@ -350,6 +367,4 @@ if __name__ == "__main__":
|
|||
if method in ["get", "post", "delete", "put", "patch"]:
|
||||
print(f" {method.upper()} {path}")
|
||||
|
||||
print(
|
||||
f"\nSchemas: {list(spec.get('components', {}).get('schemas', {}).keys())[:10]}..."
|
||||
)
|
||||
print(f"\nSchemas: {list(spec.get('components', {}).get('schemas', {}).keys())[:10]}...")
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue