Skip to content

Rhidp 14314/attachment type model validation#3629

Open
JslYoon wants to merge 3 commits into
redhat-developer:mainfrom
JslYoon:rhidp-14314/attachment-type-model-validation
Open

Rhidp 14314/attachment type model validation#3629
JslYoon wants to merge 3 commits into
redhat-developer:mainfrom
JslYoon:rhidp-14314/attachment-type-model-validation

Conversation

@JslYoon

@JslYoon JslYoon commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Hey, I just made a Pull Request!

  • Add /v1/validate-model-vision endpoint that probes model vision
    capability by sending a minimal test JPEG to LCS /v1/responses
    • Add validateAttachmentsForModel middleware on /v1/query to reject
      image attachments for non-vision models
    • Add ModelCapabilitiesCache singleton to cache vision capability
      results per model
    • Remove stray console.log debug statement from notebooks router
    • Unit tests for the cache and integration tests for both the endpoint and
      query validation
      https://redhat.atlassian.net/browse/RHIDP-14314

✔️ Checklist

  • A changeset describing the change and affected packages. (more info)
  • Added or Updated documentation
  • Tests for new functionality and regression tests for bug fixes
  • Screenshots attached (for UI changes)

@github-actions

Copy link
Copy Markdown
Contributor

This pull request adds a new top-level directory under workspaces/. Please follow Submitting a Pull Request for a New Workspace in CONTRIBUTING.md.

@rhdh-gh-app

rhdh-gh-app Bot commented Jun 29, 2026

Copy link
Copy Markdown

Important

This PR includes changes that affect public-facing API. Please ensure you are adding/updating documentation for new features or behavior.

Changed Packages

Package Name Package Path Changeset Bump Current Version
@red-hat-developer-hub/backstage-plugin-lightspeed-backend workspaces/lightspeed/plugins/lightspeed-backend minor v2.9.1

@JslYoon JslYoon marked this pull request as ready for review June 29, 2026 21:13
@rhdh-qodo-merge

Copy link
Copy Markdown

Code Review by Qodo

Grey Divider

Sorry, something went wrong

We weren't able to complete the code review on our side. Please try again

Grey Divider

Qodo Logo

@rhdh-qodo-merge

Copy link
Copy Markdown

PR Summary by Qodo

Add model vision validation and block image attachments for non-vision models

✨ Enhancement 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Add /v1/validate-model-vision endpoint to probe vision capability via a minimal JPEG request.
• Enforce image-attachment gating on /v1/query using cached per-model vision results.
• Add cache/unit tests and router integration tests covering vision validation and query rejection
 paths.
Diagram

graph TD
  C[Client] --> V["POST /v1/validate-model-vision"] --> MC[("ModelCapabilitiesCache")]
  V --> LCS["LCS /v1/responses"]
  C --> Q["POST /v1/query"] --> MW["validateAttachmentsForModel"] --> MC
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Use LCS /v1/models supports_vision metadata (no probing)
  • ➕ Lower latency and load than sending a test /v1/responses request
  • ➕ Deterministic behavior with clearer 'model not found' semantics
  • ➕ Avoids false negatives caused by transient /v1/responses errors
  • ➖ Depends on LCS accurately maintaining supports_vision for each provider/model
  • ➖ May diverge from actual runtime capability if metadata is stale
2. Lazy validation on first image query (no pre-validation endpoint)
  • ➕ Simpler client flow; no extra endpoint call required
  • ➕ Validation stays close to the real usage path
  • ➖ Adds latency to the first /v1/query with images
  • ➖ Harder to distinguish transient failures vs true lack of capability
3. Provider+model keyed cache with TTL (vs model-only, permanent cache)
  • ➕ Avoids collisions when different providers reuse the same model identifier
  • ➕ TTL reduces risk from permanent false negatives after transient outages
  • ➖ Slightly more implementation complexity (keying + eviction/expiry)

Recommendation: The overall direction (explicit capability validation + middleware gating) is sound, but the current implementation should be aligned to a clear contract: (1) decide whether validation is based on /v1/models metadata or an active /v1/responses probe and make endpoint semantics reflect that choice; (2) key the cache by provider+model (and ideally add a TTL) to avoid cross-provider collisions and long-lived false negatives. Also, the router/middleware responses should be made consistent with the tests (field naming and error messages) to avoid an API/test contract mismatch.

Files changed (6) +412 / -1

Enhancement (4) +163 / -1
attachment-validation.tsIntroduce in-memory ModelCapabilitiesCache singleton +35/-0

Introduce in-memory ModelCapabilitiesCache singleton

• Adds a simple singleton cache for storing model -> supportsVision boolean. Exposes get/set/has/clear helpers for shared use by routing and validation logic.

workspaces/lightspeed/plugins/lightspeed-backend/src/service/attachment-validation.ts

constant.tsAdd minimal base64 JPEG constant for vision probing +11/-0

Add minimal base64 JPEG constant for vision probing

• Defines TEST_VISION_JPEG as a 1x1 base64-encoded JPEG payload. Intended to be embedded as a data URL when probing model vision support.

workspaces/lightspeed/plugins/lightspeed-backend/src/service/constant.ts

router.tsWire attachment validation middleware and add /v1/validate-model-vision endpoint +75/-1

Wire attachment validation middleware and add /v1/validate-model-vision endpoint

• Adds validateAttachmentsForModel into the /v1/query pipeline after request validation. Introduces a new POST /v1/validate-model-vision endpoint that probes LCS /v1/responses using a minimal JPEG and caches the result in ModelCapabilitiesCache.

workspaces/lightspeed/plugins/lightspeed-backend/src/service/router.ts

validation.tsAdd validateAttachmentsForModel middleware for image support enforcement +42/-0

Add validateAttachmentsForModel middleware for image support enforcement

• Adds middleware that inspects attachments for image content and requires a prior cached vision validation. Rejects requests when the model was not validated or is cached as non-vision-capable; otherwise allows the request to proceed.

workspaces/lightspeed/plugins/lightspeed-backend/src/service/validation.ts

Tests (2) +249 / -0
attachment-validation.test.tsAdd unit tests for ModelCapabilitiesCache behavior +56/-0

Add unit tests for ModelCapabilitiesCache behavior

• Introduces unit coverage for setting, retrieving, updating, and clearing cached vision capability values per model. Ensures unknown models return undefined and has() behaves correctly.

workspaces/lightspeed/plugins/lightspeed-backend/src/service/attachment-validation.test.ts

router.test.tsAdd integration tests for model vision validation and query attachment gating +193/-0

Add integration tests for model vision validation and query attachment gating

• Extends router integration tests to cover the new /v1/validate-model-vision endpoint and attachment behavior on /v1/query. Clears the ModelCapabilitiesCache between cases to avoid cross-test contamination.

workspaces/lightspeed/plugins/lightspeed-backend/src/service/router.test.ts

@rhdh-qodo-merge rhdh-qodo-merge Bot added enhancement New feature or request Tests labels Jun 29, 2026
Add backend validation for image attachments, including a model vision
capability check via /v1/validate-model-vision endpoint and middleware
to reject attachments for non-vision models on /v1/query.

RHIDP-14314

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Lucas <lyoon@redhat.com>
@JslYoon JslYoon force-pushed the rhidp-14314/attachment-type-model-validation branch 2 times, most recently from 4171980 to 27c2c65 Compare July 1, 2026 19:39
@JslYoon JslYoon force-pushed the rhidp-14314/attachment-type-model-validation branch from 27c2c65 to 2d859f5 Compare July 1, 2026 19:51
@codecov

codecov Bot commented Jul 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.90909% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 54.03%. Comparing base (03662b1) to head (31ee1ba).
⚠️ Report is 7 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3629      +/-   ##
==========================================
+ Coverage   54.00%   54.03%   +0.02%     
==========================================
  Files        2328     2329       +1     
  Lines       89277    89336      +59     
  Branches    24979    24987       +8     
==========================================
+ Hits        48218    48271      +53     
- Misses      39452    39458       +6     
  Partials     1607     1607              
Flag Coverage Δ *Carryforward flag
adoption-insights 83.70% <ø> (ø) Carriedforward from 2d859f5
ai-integrations 67.95% <ø> (ø) Carriedforward from 2d859f5
app-defaults 69.79% <ø> (ø) Carriedforward from 2d859f5
augment 46.39% <ø> (ø) Carriedforward from 2d859f5
boost 73.51% <ø> (ø) Carriedforward from 2d859f5
bulk-import 72.46% <ø> (ø) Carriedforward from 2d859f5
cost-management 14.10% <ø> (ø) Carriedforward from 2d859f5
dcm 61.81% <ø> (ø) Carriedforward from 2d859f5
extensions 61.53% <ø> (ø) Carriedforward from 2d859f5
global-floating-action-button 71.18% <ø> (ø) Carriedforward from 2d859f5
global-header 59.71% <ø> (ø) Carriedforward from 2d859f5
homepage 49.84% <ø> (ø) Carriedforward from 2d859f5
install-dynamic-plugins 56.77% <ø> (ø) Carriedforward from 2d859f5
konflux 91.49% <ø> (ø) Carriedforward from 2d859f5
lightspeed 68.72% <90.90%> (+0.22%) ⬆️
mcp-integrations 85.46% <ø> (ø) Carriedforward from 2d859f5
orchestrator 37.01% <ø> (ø) Carriedforward from 2d859f5
quickstart 65.63% <ø> (ø) Carriedforward from 2d859f5
sandbox 79.56% <ø> (ø) Carriedforward from 2d859f5
scorecard 82.67% <ø> (ø) Carriedforward from 2d859f5
theme 61.26% <ø> (ø) Carriedforward from 2d859f5
translations 7.25% <ø> (ø) Carriedforward from 2d859f5
x2a 78.68% <ø> (ø) Carriedforward from 2d859f5

*This pull request uses carry forward flags. Click here to find out more.


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 03662b1...31ee1ba. Read the comment docs.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@JslYoon JslYoon requested a review from its-mitesh-kumar July 1, 2026 20:14
@JslYoon JslYoon force-pushed the rhidp-14314/attachment-type-model-validation branch from 8a5871f to a6b43f4 Compare July 1, 2026 20:20
},
);
const supportsVision = testResponse.ok;
ModelCapabilitiesCache.set(model, supportsVision);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Shouldn't we also include provider in the Key for cache.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updated!

Signed-off-by: Lucas <lyoon@redhat.com>
@JslYoon JslYoon force-pushed the rhidp-14314/attachment-type-model-validation branch from a6b43f4 to 31ee1ba Compare July 2, 2026 18:49
@sonarqubecloud

sonarqubecloud Bot commented Jul 2, 2026

Copy link
Copy Markdown

@its-mitesh-kumar

Copy link
Copy Markdown
Member

/fs-review

@fullsend-ai-review

fullsend-ai-review Bot commented Jul 2, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 10:55 PM UTC · Completed 11:12 PM UTC
Commit: d903ed7 · View workflow run →

@fullsend-ai-review

Copy link
Copy Markdown

Review

Findings

High

  • [merge-conflict-risk] workspaces/lightspeed/plugins/lightspeed-backend/src/service/router.ts:598 — The diff context for /v1/query shows middleware order as express.json, validateCompletionsRequest, validateAttachmentsForModel, requirePermission — but the current main branch has expensiveRateLimiter between express.json and validateCompletionsRequest. The PR branch is based on a pre-rate-limiting revision. Merging will likely cause a conflict, or if auto-merged, could silently drop the rate limiter from /v1/query.
    Remediation: Rebase onto current main.

Medium

  • [error-handling-gap] workspaces/lightspeed/plugins/lightspeed-backend/src/service/router.ts — The /v1/validate-model-vision endpoint catches all errors and caches false (model does not support vision). Any transient error (network failure, timeout, 5xx) permanently caches the model as not supporting vision since ModelCapabilitiesCache has no TTL. A transient failure permanently blocks image attachments for that model until backend restart. The catch block also responds with 200 { supportsVision: false } instead of following the codebase's established 500 error pattern.
    Remediation: Do not cache false on transient errors. Only cache false on explicit 4xx rejection from the LLM. Or add a TTL to negative cache entries.

  • [unbounded-cache] workspaces/lightspeed/plugins/lightspeed-backend/src/service/attachment-validation.ts:18ModelCapabilitiesCache is a module-level plain object with no TTL, max size, or eviction policy. Any authenticated user with lightspeedChatReadPermission can call /v1/validate-model-vision with arbitrary model/provider strings, each creating a new cache entry. An attacker could exhaust server memory. The lack of TTL also means stale results are served indefinitely if a model's capabilities change.
    Remediation: Use a bounded LRU cache with a max size (~1000 entries) and a TTL (~1 hour). Validate that model/provider values match expected patterns before caching.

  • [missing-rate-limiting] workspaces/lightspeed/plugins/lightspeed-backend/src/service/router.ts:760 — The /v1/validate-model-vision endpoint has no rate limiter middleware, unlike every other POST endpoint in this router (which use either expensiveRateLimiter or generalRateLimiter). Each uncached call triggers an outbound LLM request.
    Remediation: Add expensiveRateLimiter to the /v1/validate-model-vision middleware chain.

  • [two-step-protocol] workspaces/lightspeed/plugins/lightspeed-backend/src/service/validation.ts:109 — The validateAttachmentsForModel middleware requires clients to call /v1/validate-model-vision before /v1/query when sending image attachments. This coupling through shared mutable in-memory cache state is fragile: if the backend restarts between the validate and query calls, the user gets a confusing 400 error.
    Remediation: Consider performing the vision check inline in the validation middleware (with caching as an optimization) rather than requiring a separate pre-flight call.

  • [scope-creep] workspaces/lightspeed/plugins/lightspeed-backend/src/service/router.ts:607 — The PR adds attachment content processing logic in /v1/query that prepends non-image attachment content to the query string and deletes the attachments field from the request body. While the changeset mentions "capability to add attachments on /v1/query", this query-mutation behavior could benefit from more explicit documentation in the PR description.

  • [interface-breaking-change] workspaces/lightspeed/plugins/lightspeed-backend/src/service/types.ts:62QueryRequestBody.attachments changes from {name, content} to {attachment_type, content_type, content}. The frontend already uses the new shape, so this aligns backend with frontend. However, the changeset marks this as minor rather than a breaking/major change. Any other consumer sending the old shape will silently break.
    Remediation: Confirm no other API consumers use the old shape. Consider adding validation that rejects the old shape with a clear error message.

Low

  • [middleware-ordering] router.ts:600validateAttachmentsForModel runs before requirePermission on /v1/query. An authenticated but unauthorized user can trigger validation logic and observe different error messages based on cache state, though the information leakage is minimal.
  • [input-validation] router.ts:762 — No validation that model and provider are present, non-empty strings before constructing the cache key and outbound request.
  • [info-disclosure] validation.ts:127 — Error message "Please call /v1/validate-model-vision first" leaks internal endpoint path.
  • [authorization-scope] router.ts:760/v1/validate-model-vision uses lightspeedChatReadPermission but triggers actual LLM inference (cost implications).
  • [inline-types] router.ts:618 — Repeated inline type annotations for attachment arrays instead of referencing QueryRequestBody or extracting a named Attachment type.
  • [express-json] router.ts:757/v1/validate-model-vision lacks per-route express.json() with a size limit, unlike other POST endpoints.
  • [constant-placement] constant.ts:213TEST_VISION_JPEG is a base64 blob in the constants file; other constants are simple scalars.
  • [singleton-pattern] validation.ts:101validateAttachmentsForModel imports a module-level singleton cache, a new pattern; existing shared state uses router closure scope.

Labels: PR introduces new API endpoint and modifies validation middleware with security-relevant findings (missing rate limiting, unbounded cache, middleware ordering).

@fullsend-ai-review fullsend-ai-review 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.

See the review comment for full details.

@@ -593,6 +598,7 @@ export async function createRouter(
'/v1/query',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[high] merge-conflict-risk

The diff context for /v1/query shows middleware order as express.json, validateCompletionsRequest, validateAttachmentsForModel, requirePermission — but the current main branch has expensiveRateLimiter between express.json and validateCompletionsRequest. The PR branch is based on a pre-rate-limiting revision. Merging will likely cause a conflict, or if auto-merged, could silently drop the rate limiter from /v1/query.

Suggested fix: Rebase onto current main to pick up the rate-limiting middleware.

*/

export const ModelCapabilitiesCache = {
cache: {} as Record<string, boolean>,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[medium] unbounded-cache

ModelCapabilitiesCache is a module-level plain object with no TTL, max size, or eviction policy. Any authenticated user can create unlimited cache entries with arbitrary model/provider strings. Stale results are served indefinitely.

Suggested fix: Use a bounded LRU cache with max ~1000 entries and TTL (~1 hour). Validate model/provider values.

},
);

router.post(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[medium] missing-rate-limiting

The /v1/validate-model-vision endpoint has no rate limiter middleware, unlike every other POST endpoint in this router. Each uncached call triggers an outbound LLM request.

Suggested fix: Add expensiveRateLimiter to the /v1/validate-model-vision middleware chain.

req: Request,
res: Response,
next: NextFunction,
) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[medium] two-step-protocol

validateAttachmentsForModel requires clients to call /v1/validate-model-vision first. This coupling through shared mutable in-memory cache is fragile: backend restart between calls produces confusing 400 errors.

Suggested fix: Consider performing the vision check inline with caching as optimization rather than requiring a pre-flight call.

@@ -601,6 +607,27 @@ export async function createRouter(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[medium] scope-creep

PR adds attachment content processing (prepending non-image content to query, deleting attachments field) beyond stated validation scope. This query-mutation behavior could benefit from more explicit documentation.

system_prompt?: string;

// Attachments array (file content sent with the query)
// Attachments array (e.g. DOM context, screenshots)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[medium] interface-breaking-change

QueryRequestBody.attachments changes from {name, content} to {attachment_type, content_type, content}. Frontend already uses new shape but changeset marks as minor, not breaking. Other consumers would silently break.

Suggested fix: Confirm no other consumers use old shape. Add validation rejecting old shape with clear error.

response.json({ model, provider, supportsVision });
} catch (error) {
logger.error(`Vision test error for ${cacheKey}:`, error);
ModelCapabilitiesCache.set(cacheKey, false);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think we should not cache the false in case of errors, it can happen due to transient failure which will result into permanently mark the model as non-vision-capable.

},
);
const supportsVision = testResponse.ok;
ModelCapabilitiesCache.set(cacheKey, supportsVision);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should we consider adding TTL for it. I know once a model has vision support it might not change ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants