Skip to content

feat(lens): forward eval and contract dataset routes to Lens - #45503

Open
ghost wants to merge 6 commits into
mainfrom
litellm_lens_eval_forwarding
Open

ghost wants to merge 6 commits into
mainfrom
litellm_lens_eval_forwarding

Conversation

@ghost

@ghost ghost commented Oct 9, 2026

Copy link
Copy Markdown

TLDR

Problem this solves:

  • The Lens eval SDK points at the gateway, but the gateway has no eval routes
  • Eval runs and contract dataset reads return 404 through the proxy

How it solves it:

  • Forward /lens/evals/*, /lens/datasets/resolve and contract case reads to Lens
  • Each forwarded call carries a 60 second HS256 identity for the calling key

User Flow

Before: a team running lens eval in CI against the proxy cannot create a run

  1. CI sends POST https://litellm-domain/lens/evals/runs with its virtual key and X-Lens-Contract: 1
  2. The proxy answers 404, so lens eval exits with an infra error

After: the same request reaches Lens and the run is recorded for the key's team

  1. The admin sets LITELLM_LENS_URL and LITELLM_LENS_GATEWAY_SECRET (at least 32 characters) and restarts the proxy
  2. CI sends the same POST https://litellm-domain/lens/evals/runs and gets Lens's 201 with the run id
  3. GET https://litellm-domain/lens/datasets/resolve?name=&revision= and GET https://litellm-domain/lens/datasets//revisions//cases with X-Lens-Contract: 1 return the dataset and its cases
  4. The same cases URL without the header still gets the existing admin response

Pre-Submission checklist

  • I have added meaningful tests
  • The handful of test files covering my change pass locally, e.g. uv run pytest tests/unit/<your_test_file>.py -v
  • My PR passes all required CI/CD checks (e.g., lint, schema.d.ts sync check, etc.)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review

Type

🆕 New Feature

Caveats (if any)

Medium

  • Live proxy proof is still missing
    • The local demo used a small gateway stand-in, not this proxy
  • Without both env vars the routes answer 501, never a silent pass

Low

  • The identity drops models, since Lens never reads it

Final Attestation

  • The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR

Link to Devin session: https://app.devin.ai/sessions/8dd673b2547a428cab2c3582aff5a3af
Open in Devin Desktop: https://app.devin.ai/desktop/session/8dd673b2547a428cab2c3582aff5a3af?variant=devin
Requested by: @ishaan-berri

devin-ai-integration Bot and others added 3 commits October 9, 2026 03:37
… gateway identity

Co-Authored-By: Ishaan Jaffer <155045088+ishaan-berri@users.noreply.github.com>
Co-Authored-By: Ishaan Jaffer <155045088+ishaan-berri@users.noreply.github.com>
Co-Authored-By: Ishaan Jaffer <155045088+ishaan-berri@users.noreply.github.com>
@devin-ai-integration devin-ai-integration Bot assigned ghost Oct 9, 2026
@devin-ai-integration

Copy link
Copy Markdown
Contributor

I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".

  • Disable automatic comment, CI, and merge conflict monitoring

@ghost
ghost had a problem deploying to e2e-changed October 9, 2026 04:04 — with GitHub Actions Error
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

Co-Authored-By: Ishaan Jaffer <155045088+ishaan-berri@users.noreply.github.com>
@greptile-apps

greptile-apps Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 3/5

[Medium impact] Adds new routes to forward evaluation and dataset requests to Lens.

Fix the missing configuration requirement and route access before merging, and supply the required live proxy proof

Findings

  1. P1 Advertised setup leaves evals disabled ▶
  2. P1 CI keys cannot finish evals ▶
  3. P2 Real proxy proof is missing ▶

Summary

Adds Lens forwarding for eval operations, dataset resolution, and contract case reads, with a signed identity that expires after 60 seconds

  • The proxy forwards evals and marked contract reads to Lens.

Reviews (1) · Last reviewed commit: "test(lens): type the fixed-clock decode ..." · Reviewed by Greptile

Comment on lines +43 to +45
connection: Final = LensConnection.from_env(environ)
except ValueError:
return None

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.

P1 Advertised setup leaves evals disabled

Setting only LITELLM_LENS_URL and LITELLM_LENS_GATEWAY_SECRET still returns 501. LensConnection.from_env also requires LITELLM_LENS_SERVICE_TOKEN, but the setup instructions and error message omit it. Validate eval configuration separately, or document and report the third requirement

Comment on lines +125 to +127
@router.get("/lens/datasets/resolve", include_in_schema=False)
async def resolve_dataset(request: Request, auth: Auth, upstream: Upstream) -> Response:
return await forward(request, "/lens/datasets/resolve", auth, upstream)

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.

P1 CI keys cannot finish evals

Ordinary virtual keys are rejected before /lens/datasets/resolve, contract case reads, and deeper eval paths such as /lens/evals/runs/run-1/finish reach Lens. Auth runs the existing route checks, which treat these unmatched paths as admin-only. Register the new paths for the intended callers without widening access to existing admin routes

Knowledge Base Used:

Comment on lines +37 to +39
app.dependency_overrides[user_api_key_auth] = lambda: auth
app.dependency_overrides[eval_upstream] = lambda: upstream
app.dependency_overrides[dataset_store_factory] = lambda: lambda: store

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.

P2 Real proxy proof is missing

These tests replace authentication and configuration, and use MockTransport for Lens. Repository rules require live end-to-end proof through the real proxy before merging. Add that proof for eval writes, dataset reads, failures, and unchanged admin reads

Rule Used: Flag if the PR has no proof of e2e testing (if e2e testing is applicable here. If it's like an AGENTS.md change or something then probably no need to have e2e test proof) that either doesn't exist, claims that the box that it was trying to run in doe... (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!


@router.api_route("/lens/evals/{path:path}", methods=["GET", "POST", "PUT"], include_in_schema=False)
async def forward_evals(path: str, request: Request, auth: Auth, upstream: Upstream) -> Response:
return await forward(request, f"/lens/evals/{quote(path, safe='/')}", auth, upstream)

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.

Medium: Parent-directory traversal bypasses route restrictions

A key restricted to /lens/evals can request GET /lens/evals/%2e%2e/datasets/resolve?name=regressions&revision=7 and have it forwarded to /lens/datasets/resolve with a signed identity, despite the direct route being disallowed. ASGI decodes %2e%2e to .., quote() preserves the dots, and HTTPX removes the parent-directory segment before sending; reject decoded dot segments before forwarding. (asgi.readthedocs.io)

Suggested change
return await forward(request, f"/lens/evals/{quote(path, safe='/')}", auth, upstream)
if any(segment in {".", ".."} for segment in path.split("/")):
raise HTTPException(400, "Invalid eval path")
return await forward(request, f"/lens/evals/{quote(path, safe='/')}", auth, upstream)

@codspeed

codspeed Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing litellm_lens_eval_forwarding (4ee0c67) with main (4b975e6)1

Open in CodSpeed

Footnotes

  1. No successful run was found on main (72ab736) during the generation of this report, so 4b975e6 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report. ↩

@veria-ai

veria-ai Bot commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

PR overview

The PR adds proxy forwarding to Lens for evaluation and contract dataset routes.

A parent-directory traversal allows an API key restricted to evaluation routes to reach the dataset-resolution endpoint with a signed identity, bypassing route restrictions. This authorization bypass remains open, and no issues have been addressed yet.

Open issues (1)

Fixed/addressed: 0 · PR risk: 7/10

@codecov

codecov Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.29630% with 3 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
litellm/proxy/lens/eval_forwarding.py 96.20% 3 Missing ⚠️

📢 Thoughts on this report? Let us know!

Co-Authored-By: Ishaan Jaffer <155045088+ishaan-berri@users.noreply.github.com>
Co-Authored-By: Ishaan Jaffer <155045088+ishaan-berri@users.noreply.github.com>

This branch is waiting to be deployed

1 waiting deployment
e2e-changed — 4ee0c678 Waiting Oct 9, 2026 by devin-ai-integration[bot] via oauth #4013
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