Skip to content

feat(cli): let a launched pi session name its model upstream - #2

Closed
yczhang-nv wants to merge 1 commit into
feat/pi-integrationfrom
feat/pi-provider-forwarding
Closed

feat(cli): let a launched pi session name its model upstream#2
yczhang-nv wants to merge 1 commit into
feat/pi-integrationfrom
feat/pi-provider-forwarding

Conversation

@yczhang-nv

Copy link
Copy Markdown
Owner

Overview

Stacked on NVIDIA/NeMo-Relay#804. Base is feat/pi-integration, so the diff here is one commit. Opened on the fork because the upstream repository has no feat/pi-integration branch to target; retarget to main once NVIDIA#804 merges.

Lets a launched pi session tell the gateway where to forward its model traffic, so a provider the gateway was never configured for still produces LLM spans and is still subject to model-call policy.

The gateway forwards each API family to one statically configured upstream, so pi could only redirect a model whose endpoint was already that upstream. Everything else refused — upstream-mismatch or unknown-upstream — because redirecting past a mismatch does not cost spans, it reaches the wrong provider and breaks the session. That refusal is the common case for pi: 32 of its 39 providers speak an API the gateway serves, but only the two it is configured for are an endpoint some model already calls.

  • I confirm this contribution is my own work, or I have the right to submit it under this project's license.
  • I searched existing issues and open pull requests, and this does not duplicate existing work.

Details

The extension sends x-nemo-relay-upstream-base-url on each request for a redirected provider, and the gateway forwards there instead of to its configured upstream.

What makes the header safe to honor. It is a client telling a gateway where to send credentialed traffic, so the question is whether that client is the agent this invocation launched — and the transparent proxy credential answers it. The launcher mints it per run and gives it only to the process it starts, and the extension already sends it on redirected providers. Without it the header is treated as absent and configured routing applies unchanged, so a standalone nemo-relay --bind daemon, which mints no credential, is unaffected.

Deliberately not a widening of x-nemo-relay-internal-dispatch-url. That header already redirects a single request, and is stripped from every inbound request precisely so a client cannot steer the gateway — it belongs to request intercepts, which are trusted plugin code running inside it. Widening it would hand that authority to anything that can reach the port. A separate header keeps both statements true and checkable. The new one is also stripped before forwarding upstream: it addresses this gateway and means nothing to a provider.

The provider-wide constraint survives, with a different question. registerProvider rewrites every model of a provider, so naming one endpoint commits its siblings to it. The sibling scan still runs: on the static path it asks whether every model already targets the gateway's upstream, and on the named path whether every model shares the endpoint about to be named. A provider that mixes endpoints — Fireworks — is refused either way.

One existing test changed meaning rather than breaking. A mismatched upstream with a credential now registers, which is the feature. Its invariant — that a provider Relay did not redirect never sees the credential — is unchanged and still asserted, through the case that still refuses.

Where should the reviewer start?

crates/cli/src/agents/pi/alignment.rs. It is the whole trust decision in one function, and its module comment states the threat model.

Then crates/cli/tests/coverage/agents/pi_alignment_tests.rs, where an_unauthenticated_request_cannot_name_an_upstream is the security boundary written as a test, and only_absolute_http_urls_with_a_bare_host_are_accepted covers the URL shapes that would otherwise reach somewhere they should not.

Known limitations

  • A standalone nemo-relay --bind daemon still cannot reach an arbitrary provider, because it issues no credential to check against. Static configuration remains the answer there.
  • The gate is the invocation credential, not an allowlist of hosts. That is a deliberate trade: on the launcher path the credential already proves the caller is our own child process, which an allowlist would not improve. A daemon-facing allowlist is the natural follow-up if the standalone case is wanted.
  • NEMO_RELAY_PI_REDIRECT=force is unchanged and still redirects without naming an endpoint.

Validation

  • cargo nextest run -p nemo-relay-cli — 1411 passed (1402 before; 9 added).
  • just test-pi — 108 passed (99 before; 9 added), tsc clean.
  • just docs-linkcheck — 0 errors. cargo clippy --workspace --all-targets -- -D warnings clean. uv run pre-commit clean on the changed files.
  • Not yet exercised against a live third-party provider; the evidence here is unit and contract coverage on both sides of the header.

Related Issues: (use one of the action keywords Closes / Fixes / Resolves / Relates to)

🤖 Generated with Claude Code

The gateway forwards each API family to one statically configured upstream, so
pi could only redirect a model whose endpoint was already that upstream.
Everything else refused -- `upstream-mismatch` or `unknown-upstream` -- because
redirecting past a mismatch does not cost spans, it sends the request to the
wrong provider and breaks the session. That refusal is the common case for pi:
32 of its 39 providers speak an API the gateway serves, but only the two it is
configured for are an endpoint some model already calls.

A launched session can now name the endpoint, in `x-nemo-relay-upstream-base-url`
on each request, so one gateway fronts a provider it was never configured for and
those models get LLM spans and model-call enforcement.

**What makes the header safe to honor.** It is a client telling a gateway where
to send credentialed traffic, so the question is whether the client is the agent
this invocation launched, and the transparent proxy credential is what answers
it: minted per run, given only to the process the launcher starts, and already
sent on redirected providers since `a16c9659`. Without it the header is treated
as absent and configured routing applies unchanged, so a standalone
`nemo-relay --bind` daemon -- which mints no credential -- is unaffected.

Deliberately *not* a widening of `x-nemo-relay-internal-dispatch-url`. That one
redirects a single request and is stripped from every inbound request precisely
so a client cannot steer the gateway; it belongs to request intercepts, which are
trusted plugin code. A separate header keeps both statements true and checkable.
The new one is also stripped before forwarding: it addresses this gateway, and
means nothing to a provider.

**The provider-wide constraint survives, with a different question.**
`registerProvider` rewrites every model of a provider, so naming one endpoint
commits its siblings to it. The sibling scan therefore still runs; on the static
path it asks whether every model already targets the gateway's upstream, and on
the named path whether every model shares the endpoint about to be named. A
provider that mixes endpoints is refused either way, which is the same broken
session the static path exists to prevent.

One existing test changed meaning rather than breaking: a mismatched upstream
*with* a credential now registers, which is the feature. Its invariant -- that a
provider Relay did not redirect never sees the credential -- is unchanged and
still asserted, now via the case that still refuses.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Yuchen Zhang <yuchenz@nvidia.com>
@yczhang-nv

Copy link
Copy Markdown
Owner Author

Closing: this should target the upstream repository, not the fork. The branch stays for review; the formal PR will be opened against NVIDIA/NeMo-Relay.

@yczhang-nv yczhang-nv closed this Sep 1, 2026
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.

1 participant