NVIDIA-NeMo / NVIDIA-NeMo/Switchyard

[bug] Filter unsupported input modalities per target

Open
#152 3 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Rust
Stars
3.2k
Forks
291
Avg merge
1d 8h
Merged PRs (30d)
182

Description

Symptom

A coding-agent trajectory containing rich input is forwarded unchanged after
routing, even when the selected model cannot accept that modality. The
upstream rejects the request instead of Switchyard removing unsupported
content for that target.

Switchyard also has no target configuration field that declares accepted input
modalities. An image-only boolean would solve the immediate reproduction but
would repeat the problem for audio, video, and files.

Reproduction

At commit 0a03235b, export a valid NVIDIA Inference Hub credential:

export NVIDIA_API_KEY="..."

Run the same OpenAI Responses request through two OpenAI-compatible
Switchyard targets:

import asyncio
import base64
import os
from pathlib import Path

from switchyard import ChatRequest, ProxyContext
from switchyard.lib.backends.llm_target import (
    BackendFormat,
    LlmTarget,
    llm_target_with_runtime_defaults,
)
from switchyard_rust.components import OpenAiNativeBackend


async def main() -> None:
    image = base64.b64encode(Path("assets/logo.png").read_bytes()).decode()
    request = ChatRequest.openai_responses({
        "model": "switchyard",
        "input": [{
            "type": "message",
            "role": "user",
            "content": [
                {"type": "input_text", "text": "Describe this image briefly."},
                {
                    "type": "input_image",
                    "image_url": f"data:image/png;base64,{image}",
                },
            ],
        }],
    })

    for target_id, model in [
        ("opus", "azure/anthropic/claude-opus-4-7"),
        ("nemotron", "nvidia/nvidia/nemotron-3-super-v3"),
    ]:
        target = llm_target_with_runtime_defaults(LlmTarget(
            id=target_id,
            model=model,
            format=BackendFormat.RESPONSES,
            base_url="https://inference-api.nvidia.com/v1",
            api_key=os.environ["NVIDIA_API_KEY"],
        ))
        backend = OpenAiNativeBackend(target)
        try:
            response = await backend.call(ProxyContext(), request)
            print(model, "OK", response.to_body())
        except Exception as error:
            print(model, "ERROR", error)


asyncio.run(main())

Observed on July 27, 2026:

  • azure/anthropic/claude-opus-4-7 succeeded and described the Switchyard logo.
  • nvidia/nvidia/nemotron-3-super-v3 returned HTTP 400 indicating that it was
    not a multimodal model.

A later negative control returned a more generic HTTP 400 validation response
when the image was preserved, but succeeded after Switchyard removed the image.

Expected vs. actual

  • Expected: A target can declare accepted input modalities. Switchyard
    removes recognized unsupported content from its cloned outbound request,
    preserves supported content and message structure, and leaves the inbound
    trajectory untouched for retries or later targets.
  • Actual: LlmTarget has no input-capability metadata. Rich content is
    preserved and forwarded to every selected target.

Proposed contract

Add an optional input_modalities allowlist to LlmTarget and supported
configuration surfaces:

strong:
  model: azure/anthropic/claude-opus-4-7
  input_modalities: [text, image]
weak:
  model: nvidia/nvidia/nemotron-3-super-v3
  input_modalities: [text]

Initial typed values:

  • text
  • image
  • audio
  • video
  • file

Semantics:

  • An explicit list is authoritative. Recognized content whose modality is
    absent is removed after target selection and format translation.
  • Omission means capabilities are unknown and preserves existing pass-through
    behavior.
  • Invalid literals fail configuration loudly.
  • Filtering is target-local and does not mutate the original request.
  • Unknown provider-extension blocks and unrelated JSON such as tool schemas
    are preserved by the filtering step.

This metadata does not need to make routing modality-aware in the first
change. Automatic capability discovery and reject/reroute policies can be
follow-up work.

Environment

  • Switchyard commit SHA: 0a03235b
  • Python version: 3.14.3
  • OS / arch: macOS / arm64
  • Install path: source build with uv
  • Inbound and backend format: OpenAI Responses
  • Backend: NVIDIA Inference Hub

Additional context

The translation crate already has internal target capabilities for images,
audio, video, and files, but shipping native backends construct the default
unknown policy. Same-format native requests also bypass translation, so a
translation-only fix would not cover all paths.

A validated spike implementing the proposed contract is available on Ryan
Angilly's fork in ryana/Switchyard#2.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start at LlmTarget, llm_target_with_runtime_defaults, and OpenAiNativeBackend, then trace the translation crate’s target-capability handling and same-format path. Review the spike in ryana/Switchyard#2 and verify that configuration accepts the allowlist, filtering is target-local, and the reproduction preserves the inbound request while removing unsupported recognized modalities.

Written by the indexing model from the issue text.

Assessment

Tech stack
python, rust, yaml
Domain
api, backend, backend-api-design
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Clearly specified
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.