mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
test(ci): serve /moderations from the canned OpenAI mock (#37739)
* test(ci): serve /moderations from the canned OpenAI mock The otel proxy E2E job points its `openai/*` wildcard deployment at the canned mock, and #37492 made `get_model_list` agree with `get_available_deployment` on bare model names. /moderations now resolves `omni-moderation-latest` to that wildcard deployment the way /chat/completions already did, so the request lands on the mock, which never implemented the route and answers a bare 404. Add /moderations and /v1/moderations to the mock, returning an OpenAI-shaped response with one result per input item. * style(ci): annotate the new moderations locals as Final
This commit is contained in:
parent
2a863f8bdd
commit
0c2e404be3
2 changed files with 60 additions and 5 deletions
|
|
@ -8,11 +8,11 @@ those jobs failed with ``404 Application not found`` even though nothing in the
|
||||||
PR was broken.
|
PR was broken.
|
||||||
|
|
||||||
This process is the local stand-in. A model points its ``api_base`` here and
|
This process is the local stand-in. A model points its ``api_base`` here and
|
||||||
gets back a well-formed chat/text/embedding response with realistic ``usage`` so
|
gets back a well-formed chat/text/embedding/moderation response with realistic
|
||||||
cost tracking and spend accounting still exercise their real code paths. The one
|
``usage`` so cost tracking and spend accounting still exercise their real code
|
||||||
behavioral special case mirrors the old hosted mock: a request whose ``model``
|
paths. The one behavioral special case mirrors the old hosted mock: a request
|
||||||
is ``429`` returns HTTP 429 so rate-limit and cooldown tests still have
|
whose ``model`` is ``429`` returns HTTP 429 so rate-limit and cooldown tests
|
||||||
something to trip on.
|
still have something to trip on.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
@ -35,6 +35,21 @@ _SLOW_MODEL: Final = "slow-endpoint"
|
||||||
_SLOW_RESPONSE_SECONDS: Final = 3.0
|
_SLOW_RESPONSE_SECONDS: Final = 3.0
|
||||||
_PROMPT_TOKENS: Final = 20
|
_PROMPT_TOKENS: Final = 20
|
||||||
_COMPLETION_TOKENS: Final = 20
|
_COMPLETION_TOKENS: Final = 20
|
||||||
|
_MODERATION_CATEGORIES: Final = (
|
||||||
|
"harassment",
|
||||||
|
"harassment/threatening",
|
||||||
|
"hate",
|
||||||
|
"hate/threatening",
|
||||||
|
"illicit",
|
||||||
|
"illicit/violent",
|
||||||
|
"self-harm",
|
||||||
|
"self-harm/instructions",
|
||||||
|
"self-harm/intent",
|
||||||
|
"sexual",
|
||||||
|
"sexual/minors",
|
||||||
|
"violence",
|
||||||
|
"violence/graphic",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _usage() -> dict[str, int]:
|
def _usage() -> dict[str, int]:
|
||||||
|
|
@ -220,6 +235,28 @@ async def triton_embeddings(_request: Request) -> Response:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _moderation_result() -> dict[str, object]:
|
||||||
|
return {
|
||||||
|
"flagged": False,
|
||||||
|
"categories": {category: False for category in _MODERATION_CATEGORIES},
|
||||||
|
"category_scores": {category: 0.0 for category in _MODERATION_CATEGORIES},
|
||||||
|
"category_applied_input_types": {category: ["text"] for category in _MODERATION_CATEGORIES},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def moderations(request: Request) -> Response:
|
||||||
|
body: Final = await _parse_body(request)
|
||||||
|
raw_input: Final = body.get("input", "")
|
||||||
|
count: Final = len(raw_input) if isinstance(raw_input, list) else 1
|
||||||
|
return JSONResponse(
|
||||||
|
{
|
||||||
|
"id": f"modr-{uuid.uuid4().hex[:24]}",
|
||||||
|
"model": _requested_model(body),
|
||||||
|
"results": [_moderation_result() for _ in range(max(count, 1))],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def list_models(_request: Request) -> Response:
|
async def list_models(_request: Request) -> Response:
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
{
|
{
|
||||||
|
|
@ -247,6 +284,8 @@ app = Starlette(
|
||||||
Route("/embeddings", embeddings, methods=["POST"]),
|
Route("/embeddings", embeddings, methods=["POST"]),
|
||||||
Route("/v1/embeddings", embeddings, methods=["POST"]),
|
Route("/v1/embeddings", embeddings, methods=["POST"]),
|
||||||
Route("/triton/embeddings", triton_embeddings, methods=["POST"]),
|
Route("/triton/embeddings", triton_embeddings, methods=["POST"]),
|
||||||
|
Route("/moderations", moderations, methods=["POST"]),
|
||||||
|
Route("/v1/moderations", moderations, methods=["POST"]),
|
||||||
Route("/models", list_models, methods=["GET"]),
|
Route("/models", list_models, methods=["GET"]),
|
||||||
Route("/v1/models", list_models, methods=["GET"]),
|
Route("/v1/models", list_models, methods=["GET"]),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -13,9 +13,11 @@ from __future__ import annotations
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Final
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
import pytest
|
import pytest
|
||||||
|
from openai.types import ModerationCreateResponse
|
||||||
|
|
||||||
from tests.fake_openai_endpoint import (
|
from tests.fake_openai_endpoint import (
|
||||||
_LOCAL_DEFAULT,
|
_LOCAL_DEFAULT,
|
||||||
|
|
@ -56,6 +58,20 @@ def test_chat_completion_shape():
|
||||||
assert body["usage"]["total_tokens"] == 40
|
assert body["usage"]["total_tokens"] == 40
|
||||||
|
|
||||||
|
|
||||||
|
def test_moderations_route_parses_as_an_openai_response():
|
||||||
|
base: Final = ensure_fake_openai_endpoint()
|
||||||
|
response: Final = httpx.post(
|
||||||
|
f"{base}/v1/moderations",
|
||||||
|
json={"input": ["I want to harm someone", "hello"], "model": "omni-moderation-latest"},
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
parsed: Final = ModerationCreateResponse.model_validate(response.json())
|
||||||
|
assert parsed.model == "omni-moderation-latest"
|
||||||
|
assert len(parsed.results) == 2
|
||||||
|
assert parsed.results[0].categories.violence is False
|
||||||
|
|
||||||
|
|
||||||
def test_triton_embeddings_route():
|
def test_triton_embeddings_route():
|
||||||
base = ensure_fake_openai_endpoint()
|
base = ensure_fake_openai_endpoint()
|
||||||
response = httpx.post(f"{base}/triton/embeddings", json={"inputs": []}, timeout=10)
|
response = httpx.post(f"{base}/triton/embeddings", json={"inputs": []}, timeout=10)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue