Skip to content

fix: filter target input modalities - #367

Open
glamr-agent wants to merge 2 commits into
NVIDIA-NeMo:mainfrom
glamr-agent:feat/filter-input-modalities--1800e1662dfe
Open

fix: filter target input modalities#367
glamr-agent wants to merge 2 commits into
NVIDIA-NeMo:mainfrom
glamr-agent:feat/filter-input-modalities--1800e1662dfe

Conversation

@glamr-agent

@glamr-agent glamr-agent commented Aug 11, 2026

Copy link
Copy Markdown

Implements target-local input modality allowlists for issue #152 and refactors JSON modality parsing to avoid duplicate helpers.

Assigned issue: #152

Summary

  • Adds typed input_modalities values for text, image, audio, video, and file.
  • Threads optional target allowlists through server TOML config, LlmTarget, ModelConfig, and Python target bindings/stubs.
  • Filters recognized unsupported modalities on the cloned outbound request and encoded provider body, preserving unknown extension blocks and tool schemas.
  • Unifies OpenAI Chat, OpenAI Responses, and Anthropic JSON modality detection through one format-aware helper.

Validation

  • cargo fmt --check
  • cargo test -p switchyard-llm-client input_modalities failed because the runner lacks a system C linker: linker cc not found.
  • cargo test failed for the same environment reason: linker cc not found.

Notes

  • Upstream push to NVIDIA-NeMo/Switchyard was denied for glamr-agent, so the follow-up commit was pushed to the existing PR branch in glamr-agent/Switchyard.

Signed-off-by: Coding Agent <svc-glamr@nvidia.com>
@glamr-agent
glamr-agent requested a review from a team as a code owner August 11, 2026 19:40
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The change adds InputModality support to targets and model configurations. It propagates modality allowlists through server and Python APIs, filters neutral and provider-specific request bodies, and updates related tests and fixtures.

Changes

Input modality support

Layer / File(s) Summary
Modality contracts
crates/protocol/src/llm.rs, crates/libsy/src/core/algorithm.rs
Adds the serializable InputModality enum and an optional modality allowlist to LlmTarget.
Client filtering and request encoding
crates/libsy-llm-client/src/client.rs
Adds modality-aware model configuration, shared backend resolution, recursive neutral-IR filtering, and filtering for OpenAI Chat, OpenAI Responses, and Anthropic JSON bodies.
Configuration and language bindings
crates/switchyard-server/src/config.rs, crates/switchyard-py/src/libsy_bindings.rs, switchyard_rust/libsy.py
Accepts, validates, types, and propagates supported modality values through server and Python target configuration.
Target fixture updates
crates/libsy/src/algorithms/*, crates/switchyard-server/tests/server.rs
Initializes the new optional target field in existing test fixtures.
Estimated code review effort: 4 (Complex) ~45 minutes

Poem

I filter bright images from the stream,
Keep text and tools aligned and clean.
Five modalities now know their way,
Through Rust and Python, night and day.
— A rabbit with a tidy request

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 47.37% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: filtering target input modalities.

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
crates/libsy-llm-client/src/client.rs (1)

1296-1396: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider extending coverage to the other two wire formats.

The test covers filter_openai_responses_body only. filter_anthropic_body and filter_openai_chat_body use different block type strings (image and document for Anthropic, image_url and file for Chat). A typo in either mapper silently disables filtering for that provider, and no test would fail.

Add one case per format. The Anthropic case also exercises the system array branch and the tool_result recursion, which the Responses case does not reach.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/libsy-llm-client/src/client.rs` around lines 1296 - 1396, The existing
input-modality preservation test only covers OpenAI Responses; add separate
cases for filter_anthropic_body and filter_openai_chat_body. Exercise each
format’s image/document or image_url/file block mappings, assert unsupported
modalities are removed while text and vendor extensions remain, and ensure the
Anthropic case includes system-array handling and recursive tool_result content
filtering.
crates/switchyard-py/src/libsy_bindings.rs (1)

146-157: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The modality name table is duplicated across crates.

parse_input_modality hardcodes the five names. crates/protocol/src/llm.rs derives the same names from #[serde(rename_all = "snake_case")]. The two must stay in sync by hand.

If a variant is added to InputModality, this function still compiles and rejects the new name at the Python boundary with a confusing error. Derive the mapping from one source instead. A FromStr implementation on InputModality in the protocol crate, reused here, keeps the names in one place.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/switchyard-py/src/libsy_bindings.rs` around lines 146 - 157, Replace
the hardcoded string match in parse_input_modality with the shared InputModality
FromStr implementation from the protocol crate, converting its parse failure
into PyValueError while preserving the existing PyResult contract. Add the
FromStr mapping beside InputModality in crates/protocol/src/llm.rs, using the
serde snake_case names and an appropriate invalid-input error so future variants
remain synchronized automatically.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/libsy-llm-client/src/client.rs`:
- Around line 702-712: Prevent modality filtering from leaving Anthropic
messages with empty content: update filter_content_blocks in
crates/libsy-llm-client/src/client.rs#L702-L712 to report emptied content and
remove the affected Message or InstructionBlock from request.messages or
request.instructions; apply the same policy in filter_json_content_array at
crates/libsy-llm-client/src/client.rs#L801-L815 for nested tool_result and
top-level content arrays. In
crates/switchyard-py/src/libsy_bindings.rs#L105-L134, reject an empty
input_modalities list with PyValueError so it cannot strip every recognized
block.

---

Nitpick comments:
In `@crates/libsy-llm-client/src/client.rs`:
- Around line 1296-1396: The existing input-modality preservation test only
covers OpenAI Responses; add separate cases for filter_anthropic_body and
filter_openai_chat_body. Exercise each format’s image/document or image_url/file
block mappings, assert unsupported modalities are removed while text and vendor
extensions remain, and ensure the Anthropic case includes system-array handling
and recursive tool_result content filtering.

In `@crates/switchyard-py/src/libsy_bindings.rs`:
- Around line 146-157: Replace the hardcoded string match in
parse_input_modality with the shared InputModality FromStr implementation from
the protocol crate, converting its parse failure into PyValueError while
preserving the existing PyResult contract. Add the FromStr mapping beside
InputModality in crates/protocol/src/llm.rs, using the serde snake_case names
and an appropriate invalid-input error so future variants remain synchronized
automatically.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: d99fd085-1a34-4931-818f-3ba0091cc588

📥 Commits

Reviewing files that changed from the base of the PR and between ad8478b and 4c54433.

📒 Files selected for processing (14)
  • crates/libsy-llm-client/src/client.rs
  • crates/libsy/src/algorithms/fall_through.rs
  • crates/libsy/src/algorithms/llm_class.rs
  • crates/libsy/src/algorithms/passthrough.rs
  • crates/libsy/src/algorithms/rand.rs
  • crates/libsy/src/algorithms/stage.rs
  • crates/libsy/src/algorithms/subagent_affinity_tests.rs
  • crates/libsy/src/algorithms/util/llm_judge.rs
  • crates/libsy/src/core/algorithm.rs
  • crates/protocol/src/llm.rs
  • crates/switchyard-py/src/libsy_bindings.rs
  • crates/switchyard-server/src/config.rs
  • crates/switchyard-server/tests/server.rs
  • switchyard_rust/libsy.py

Comment on lines +702 to +712
fn filter_content_blocks(content: &mut Vec<ContentBlock>, allowed: &BTreeSet<InputModality>) {
for block in content.iter_mut() {
if let ContentBlock::ToolResult(result) = block {
filter_content_blocks(&mut result.content, allowed);
}
}
content.retain(|block| match block_input_modality(block) {
Some(modality) => allowed.contains(&modality),
None => true,
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Modality filtering can empty a message content array, and no site guards against it. Every filter helper calls retain on a content container without checking whether the container becomes empty. Anthropic rejects a message whose content is []. This file already treats that state as a defect: anthropic_requests_drop_unsigned_thinking_blocks asserts message.get("content") != Some(&json!([])). Choose one policy — drop the emptied message, or insert a placeholder text block when Text is allowed — and apply it at all three sites.

  • crates/libsy-llm-client/src/client.rs#L702-L712: after content.retain(...) in filter_content_blocks, handle the case where content is now empty, and propagate that decision to the caller so an emptied Message or InstructionBlock is removed from request.messages or request.instructions.
  • crates/libsy-llm-client/src/client.rs#L801-L815: apply the same policy after content.retain(...) in filter_json_content_array, covering both the nested tool_result content and the top-level message content.
  • crates/switchyard-py/src/libsy_bindings.rs#L105-L134: an empty input_modalities list currently produces Some(vec![]), which strips every recognized block and makes the empty-content case certain. Either reject an empty list with a PyValueError, or document that an empty list means "allow no recognized modality" once the emptied-content policy above is in place.
📍 Affects 2 files
  • crates/libsy-llm-client/src/client.rs#L702-L712 (this comment)
  • crates/libsy-llm-client/src/client.rs#L801-L815
  • crates/switchyard-py/src/libsy_bindings.rs#L105-L134
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/libsy-llm-client/src/client.rs` around lines 702 - 712, Prevent
modality filtering from leaving Anthropic messages with empty content: update
filter_content_blocks in crates/libsy-llm-client/src/client.rs#L702-L712 to
report emptied content and remove the affected Message or InstructionBlock from
request.messages or request.instructions; apply the same policy in
filter_json_content_array at crates/libsy-llm-client/src/client.rs#L801-L815 for
nested tool_result and top-level content arrays. In
crates/switchyard-py/src/libsy_bindings.rs#L105-L134, reject an empty
input_modalities list with PyValueError so it cannot strip every recognized
block.

Signed-off-by: Coding Agent <svc-glamr@nvidia.com>
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.

1 participant