Skip to content

feat: add native audio input encoding to provider formats - #77

Merged
frostming merged 2 commits into
mainfrom
feat/bub-multimodal-input
Oct 10, 2026
Merged

frostming merged 2 commits into
mainfrom
feat/bub-multimodal-input

Conversation

@PsiACE

@PsiACE PsiACE commented Oct 9, 2026 •

Copy link
Copy Markdown
Member

Republic represents audio input with Audio and audio(), using the same MIME type and byte/URL source contract as Image and Video. Applications can pass these parts through chat(), stream(), and attached conversation history.

Gemini encodes inline audio and remote references. Chat Completions encodes inline WAV/MP3; OpenRouter extends the mapping for its documented audio formats. Unsupported format/source combinations raise UnsupportedFeatureError before sending a request. Signed media URLs infer MIME types from their path, and the package includes py.typed for downstream type checking.

Validation:

  • 419 tests passed, including doctests.
  • Lock validation, all-file prek, and ty check passed.
  • Strict documentation build passed.
  • HTTP transport tests verify encoded audio from files, data URLs and bytes, streamed replies, follow-up history replay, signed image/audio/video URLs, and unsupported-input errors. These checks do not require live model generation.

@PsiACE PsiACE changed the title feat: support Bub audio inputs and Codex history migration feat: standardize audio input and configurable Codex login home Oct 9, 2026
@PsiACE PsiACE changed the title feat: standardize audio input and configurable Codex login home feat: add native audio input encoding to provider formats Oct 9, 2026
@PsiACE
PsiACE force-pushed the feat/bub-multimodal-input branch from dde0fcd to 449debd Compare October 9, 2026 16:47
@frostming
frostming marked this pull request as ready for review October 10, 2026 01:05

@github-actions github-actions Bot left a comment

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.

Verdict: needs changes — a .wav/.aiff file cannot reach Gemini with the inferred media type.

Loading audio from a path, a URL, or a WAV/AIFF data URL yields the legacy aliases audio/x-wav / audio/x-aiff, which the Chat Completions encoder canonicalizes (audio/x-wav → wav) but the Gemini encoder forwards verbatim. The documented Google example (republic.audio("path/to/audio.wav") with google:MODEL_ID) therefore sends a MIME type outside Gemini's supported audio set. Evidence and reproduction are in the comment on src/republic/formats/gemini.py.

A separate, low-impact note on the _guess_media_type change is attached to src/republic/_content.py.

return {"role": "user", "parts": [_function_response(result) for result in message.tool_results]}
parts: list[Mapping[str, Any]] = provider_payloads(message, GeminiFormat.name)
parts.extend(_part(part) for part in message.parts if isinstance(part, Text | Image | Video))
parts.extend(_part(part) for part in message.parts if isinstance(part, Text | Image | Audio | Video))

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.

Audio is admitted for Gemini here, but _part() forwards part.media_type verbatim to inlineData/fileData mimeType, and the type Republic infers for .wav/.aiff is one Gemini does not accept.

  • mimetypes maps .wav → audio/x-wav and .aif/.aiff → audio/x-aiff. That is CPython's built-in table (mimetypes.init(files=[]) returns the same), so it is not a property of this machine.
  • Gemini's accepted audio types are audio/wav, audio/mp3, audio/aiff, audio/aac, audio/ogg, audio/flac, audio/mpeg, audio/m4a, audio/l16, audio/opus, audio/alaw, audio/mulaw, audio/webm — the "Supported audio formats" list in the Gemini audio guide, the mime_type enum in the Interactions API reference, and Vertex's per-model MIME table agree, and none of them lists the x- aliases.
  • Chat Completions treats the same alias as supported (ChatFormat.AUDIO_FORMATS maps audio/x-wav → wav), so an identical call succeeds on OpenAI-compatible providers and fails on Gemini, including the example added at docs/reference/provider-api.md:102.
Reproduction (candidate 449debd, repository test double)
import republic
from tests.conftest import FakeService

service = FakeService()
service.reply_json({"candidates": [{"content": {"parts": [{"text": "heard"}]}, "finishReason": "STOP"}]})
model = republic.get_model("google:test", http_client=service.client())
await model.chat(republic.user("Listen", republic.audio("voice.wav")))  # real .wav file on disk
service.body()["contents"][0]["parts"]
# [{'text': 'Listen'}, {'inlineData': {'mimeType': 'audio/x-wav', 'data': 'UklGRi4uLi5XQVZFZm10IA=='}}]

# the same file through the chat encoder
# [{'type': 'text', 'text': 'Listen'}, {'type': 'input_audio', 'input_audio': {'data': '...', 'format': 'wav'}}]

No live model call was made (the review environment has no credentials); what is verified is the encoded request body, the stdlib inference, and the provider's documented MIME set.

Repair direction: canonicalize the legacy aliases before Gemini encoding (audio/x-wav → audio/wav, audio/x-aiff → audio/aiff), in the Gemini encoder or in the media loader, and cover it with a Gemini case that loads a .wav by path — the new Gemini tests always pass media_type= explicitly, so they never exercise the inferred alias. If pass-through is intended instead, the Google example needs media_type="audio/wav" plus a note that Gemini wants canonical MIME types.

Comment thread src/republic/_content.py

def _guess_media_type(media_class: type[_Media], name: str) -> str:
guessed, _ = mimetypes.guess_type(name)
path = urlsplit(name).path if name.startswith(_REMOTE_PREFIXES) else name

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.

Minor, non-blocking: this extraction is a no-op on the supported Pythons, and its only observable effect is a small regression. MimeTypes.guess_type already parses its argument as a URL and uses p.path (checked in the 3.11–3.14 stdlib sources and on the 3.12 runtime used here), so query strings and fragments were ignored before this change; tests/test_audio.py::test_signed_media_urls_reach_the_provider still passes with this line reverted.

The one input that behaves differently now is a URL whose path carries ; parameters: mimetypes.guess_type("https://cdn.example/a.wav;v=2") returned audio/x-wav, while urlsplit(...).path is a.wav;v=2 and yields None, so republic.image/audio/video(...) raises ValueError: Cannot guess the media type ... pass media_type explicitly for such URLs.

Keeping the extraction for explicitness is fine — just noting it adds no coverage, and dropping it preserves the behaviour documented above the loader.

@frostming

Copy link
Copy Markdown
Collaborator

/landing fix

The standard library reports the legacy aliases `audio/x-wav` and `audio/x-aiff`
for `.wav` and `.aiff` files, and Google accepts only the canonical names they
stand for, so the documented `republic.audio("audio.wav")` example sent a MIME
type outside Gemini's supported audio set. Canonicalize both aliases when
writing `inlineData` and `fileData` MIME types; Chat Completions keeps receiving
the alias and maps it to its `wav` format label.
@github-actions

Copy link
Copy Markdown
Contributor

Repaired in dc9e8c3, now the head of this branch.

The Gemini encoder canonicalizes the legacy aliases before writing mimeType for both inlineData and fileData (audio/x-wav → audio/wav, audio/x-aiff → audio/aiff), so the documented Google example sends a MIME type in Gemini's supported set. Chat Completions keeps receiving the alias and maps it to its wav format label, unchanged.

Validation on dc9e8c3:

  • New Gemini cases load .wav and .aiff by path, by remote URL, and through a legacy-alias data URL. Without the change they report audio/x-wav / audio/x-aiff in the encoded body (6 failures); with it they report the canonical names.
  • 426 tests passed; prek run --all-files and ty check passed.
  • Native checks passed on dc9e8c3: quality, tests-and-type-check (3.11–3.14), check-docs, build in run 38014333942 (dispatched with number=77, head=dc9e8c31f8d5e5cc7c0ce00d19aeb082ce4f46b0). Its Landing review job is still queued.

The separate non-blocking note on _guess_media_type is untouched, since keeping the URL-path extraction was accepted as-is.

@frostming
frostming merged commit d222232 into main Oct 10, 2026
15 of 16 checks passed
@frostming
frostming deleted the feat/bub-multimodal-input branch October 10, 2026 02:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants