feat(openai): Accept the input_audio content part in chat completions (#33606)

This commit is contained in:
Jason Wiemels
2026-08-19 13:37:50 -07:00
committed by GitHub
parent 746418a1ec
commit defb2a3100
6 changed files with 150 additions and 7 deletions
@@ -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"""
@@ -6,8 +6,8 @@ import tempfile
import unittest
from sglang.srt.entrypoints.openai.protocol import (
ChatCompletionMessageContentAudioPart,
ChatCompletionMessageContentAudioURL,
ChatCompletionMessageContentAudioURLPart,
ChatCompletionMessageContentImagePart,
ChatCompletionMessageContentImageURL,
ChatCompletionMessageContentTextPart,
@@ -911,7 +911,7 @@ class TestGenerateChatConv(CustomTestCase):
ChatCompletionMessageContentTextPart(
type="text", text="Transcribe this"
),
ChatCompletionMessageContentAudioPart(
ChatCompletionMessageContentAudioURLPart(
type="audio_url",
audio_url=ChatCompletionMessageContentAudioURL(
url="http://example.com/audio.wav"
@@ -925,6 +925,31 @@ class TestGenerateChatConv(CustomTestCase):
self.assertEqual(len(conv.audio_data), 1)
self.assertEqual(conv.audio_data[0], "http://example.com/audio.wav")
def test_user_message_with_inline_audio(self):
"""Inline input_audio reaches the parser as a data URI.
Built from raw dicts so the content parts go through validation the way
a request body does, which is where the conversion happens; the parser
itself only knows about `audio_url`.
"""
request = self._make_request(
[
{
"role": "user",
"content": [
{"type": "text", "text": "Transcribe this"},
{
"type": "input_audio",
"input_audio": {"data": "QUJD", "format": "wav"},
},
],
}
]
)
conv = generate_chat_conv(request, "chatml")
self.assertEqual(len(conv.audio_data), 1)
self.assertEqual(conv.audio_data[0], "data:audio/wav;base64,QUJD")
def test_user_message_image_at_prefix(self):
"""Test image_token_at_prefix=True puts image token before text."""
# Register a temporary template with image_token_at_prefix=True