feat(openai): Accept the input_audio content part in chat completions (#33606)
This commit is contained in:
@@ -16,9 +16,11 @@
|
||||
import unittest
|
||||
from typing import List, Optional
|
||||
|
||||
from pydantic import BaseModel, Field, ValidationError
|
||||
from pydantic import BaseModel, Field, TypeAdapter, ValidationError
|
||||
|
||||
from sglang.srt.entrypoints.openai.protocol import (
|
||||
ChatCompletionMessageContentAudioPart,
|
||||
ChatCompletionMessageContentAudioURLPart,
|
||||
ChatCompletionMessageContentImageURL,
|
||||
ChatCompletionRequest,
|
||||
ChatCompletionResponse,
|
||||
@@ -550,6 +552,75 @@ class TestChatCompletionRequest(unittest.TestCase):
|
||||
self.assertNotIn("json_schema", sampling_params)
|
||||
|
||||
|
||||
class TestAudioContentParts(unittest.TestCase):
|
||||
"""Test audio content parts and the input_audio conversion"""
|
||||
|
||||
def _audio_part(self, part):
|
||||
"""Validate a content part the way a request body would deliver it."""
|
||||
request = ChatCompletionRequest(
|
||||
model="test",
|
||||
messages=[{"role": "user", "content": [part]}],
|
||||
)
|
||||
return request.messages[0].content[0]
|
||||
|
||||
def test_input_audio_converted_to_data_uri(self):
|
||||
part = self._audio_part(
|
||||
{"type": "input_audio", "input_audio": {"data": "QUJD", "format": "wav"}}
|
||||
)
|
||||
# Converted during validation, so the inline type does not survive.
|
||||
self.assertIsInstance(part, ChatCompletionMessageContentAudioURLPart)
|
||||
self.assertEqual(part.type, "audio_url")
|
||||
self.assertEqual(part.audio_url.url, "data:audio/wav;base64,QUJD")
|
||||
|
||||
def test_input_audio_mp3_uses_registered_mime_type(self):
|
||||
part = self._audio_part(
|
||||
{"type": "input_audio", "input_audio": {"data": "QUJD", "format": "mp3"}}
|
||||
)
|
||||
self.assertEqual(part.audio_url.url, "data:audio/mpeg;base64,QUJD")
|
||||
|
||||
def test_audio_url_passes_through_unchanged(self):
|
||||
for url in ("http://example.com/audio.wav", "data:audio/wav;base64,QUJD"):
|
||||
with self.subTest(url=url):
|
||||
part = self._audio_part(
|
||||
{"type": "audio_url", "audio_url": {"url": url}}
|
||||
)
|
||||
self.assertEqual(part.type, "audio_url")
|
||||
self.assertEqual(part.audio_url.url, url)
|
||||
|
||||
def test_input_audio_rejects_unsupported_format(self):
|
||||
with self.assertRaises(ValidationError):
|
||||
self._audio_part(
|
||||
{
|
||||
"type": "input_audio",
|
||||
"input_audio": {"data": "QUJD", "format": "ogg"},
|
||||
}
|
||||
)
|
||||
|
||||
def test_input_audio_requires_a_payload(self):
|
||||
with self.assertRaises(ValidationError):
|
||||
self._audio_part({"type": "input_audio"})
|
||||
|
||||
def test_audio_url_requires_a_payload(self):
|
||||
with self.assertRaises(ValidationError):
|
||||
self._audio_part({"type": "audio_url"})
|
||||
|
||||
def test_schema_advertises_both_spellings(self):
|
||||
"""Accepting input_audio without publishing it would hide the feature.
|
||||
|
||||
Each variant requires its own payload, so the schema states that exactly
|
||||
one of the two forms is expected rather than leaving both optional.
|
||||
"""
|
||||
schema = TypeAdapter(ChatCompletionMessageContentAudioPart).json_schema()
|
||||
variants = {
|
||||
frozenset(schema["$defs"][ref["$ref"].rsplit("/", 1)[-1]]["required"])
|
||||
for ref in schema["anyOf"]
|
||||
}
|
||||
self.assertEqual(
|
||||
variants,
|
||||
{frozenset({"type", "audio_url"}), frozenset({"type", "input_audio"})},
|
||||
)
|
||||
|
||||
|
||||
class TestModelSerialization(unittest.TestCase):
|
||||
"""Test model serialization with hidden states"""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user