Skip to content

providers: never pair a caller-supplied base URL with the server's API key - #3

Open
egeboy35 wants to merge 1 commit into
lkk688:mainfrom
egeboy35:fix/server-key-not-paired-with-caller-url
Open

providers: never pair a caller-supplied base URL with the server's API key#3
egeboy35 wants to merge 1 commit into
lkk688:mainfrom
egeboy35:fix/server-key-not-paired-with-caller-url

Conversation

@egeboy35

Copy link
Copy Markdown

resolveBackend takes each half of the pair from a different place:

const baseUrl =
  (options.baseUrl && String(options.baseUrl).trim()) ||    // the request
  process.env[def.baseUrlEnv] || def.baseUrlDefault || "";

let apiKey = options.apiKey && String(options.apiKey);
if (!apiKey && def.keyEnv) apiKey = process.env[def.keyEnv] || "";   // the server

The request wins on the URL. The server fills in the key whenever the request omits it. So a request that supplies base_url and omits api_key gets the server's key aimed at the caller's host — and /api/agent forwards both to the sidecar, which builds OpenAI(base_url, api_key) and puts Authorization: Bearer <the server's key> on the wire.

Measured

The real resolver, nothing stubbed, with a scratch HOME holding a marker key:

normal use, no override
  baseUrl -> https://integrate.api.nvidia.com/v1
  apiKey  -> nvapi-SERVER-HELD-KEY

base_url supplied, api_key omitted
  baseUrl -> http://somewhere-else.example/v1
  apiKey  -> nvapi-SERVER-HELD-KEY          <-- to a host the caller named

app/api/chat/route.js:2 states the intent this violates:

// The NVIDIA_API_KEY stays on the server — the browser never sees it.

Which holds literally. The browser does not see the key; it chooses where the key is sent, and reads the reply.

The change

A key the server holds is applied only when the request either does not override the base URL, or names the same endpoint the server configured. Otherwise it is withheld, keyWithheld is set, and the route answers 400 naming both URLs and the variable — rather than the misleading "NVIDIA_API_KEY is not set" 500 the existing EMPTY check would produce.

Every path the Agent Lab actually uses is unchanged. AgentLab.js sends base_url for exactly two backends: custom, where the user types their own key into the UI beside the URL, and llama, whose keyEnv is null. The one behaviour change is that CUSTOM_API_KEY now requires CUSTOM_BASE_URL to be set to be used at all — documented in .env.local.example, which did not mention either variable before.

Tests

Adds lib/providers.test.mjs — 15 tests on Node's built-in runner, so the app gains no dependency.

Against this branch: 15 pass. Against lib/providers.js as it stands on main: 9 pass, 6 fail. Four of the six are behaviour:

a redirected base URL does not receive the server key
whitespace around a redirected URL does not slip the key through
a redirected URL under an env-configured endpoint is still a redirect
the custom backend withholds its key when pointed somewhere else

The other two assert on keyWithheld / serverBaseUrl, which this change adds, so they could not pass before it. The behavioural tests deliberately assert nothing about those fields, so the four above fail on what the resolver does rather than on a field that is missing. The 9 that pass either way are the ordinary paths.

node --test edgeLLM/nextjs-nemotron-app/lib/providers.test.mjs

@egeboy35
egeboy35 force-pushed the fix/server-key-not-paired-with-caller-url branch from bafcfa9 to 78460e6 Compare September 1, 2026 12:28
…I key

`resolveBackend` takes each half of the pair from a different place:

    const baseUrl =
      (options.baseUrl && String(options.baseUrl).trim()) ||   // the request
      process.env[def.baseUrlEnv] || def.baseUrlDefault || "";

    let apiKey = options.apiKey && String(options.apiKey);
    if (!apiKey && def.keyEnv) apiKey = process.env[def.keyEnv] || "";  // the server

The request wins on the URL. The server fills in the key whenever the request
omits it. A request that supplies base_url and omits api_key therefore gets
the server's key aimed at the caller's host, and `/api/agent` forwards both to
the sidecar, which builds `OpenAI(base_url, api_key)` and puts
`Authorization: Bearer <the server's key>` on the wire.

Measured by calling the real resolver with a scratch HOME holding a marker
key, nothing stubbed:

    normal use, no override
      baseUrl -> https://integrate.api.nvidia.com/v1
      apiKey  -> nvapi-SERVER-HELD-KEY

    base_url supplied, api_key omitted
      baseUrl -> http://somewhere-else.example/v1
      apiKey  -> nvapi-SERVER-HELD-KEY        <-- to a host the caller named

app/api/chat/route.js:2 states the intent this violates:

    // The NVIDIA_API_KEY stays on the server — the browser never sees it.

Which holds literally. The browser does not see the key; it chooses where the
key is sent, and reads the reply.

A key the server holds is now only applied when the request either does not
override the base URL or names the same endpoint the server configured.
Otherwise it is withheld, `keyWithheld` is set, and the route answers 400
naming both URLs and the variable, rather than the misleading "NVIDIA_API_KEY
is not set" 500 the existing EMPTY check would produce.

Every path the Agent Lab actually uses is unchanged. AgentLab.js sends
base_url for exactly two backends: `custom`, where the user types their own
key into the UI beside the URL, and `llama`, whose keyEnv is null. The one
behaviour change is that CUSTOM_API_KEY now requires CUSTOM_BASE_URL to be set
to be used at all -- documented in .env.local.example, which did not mention
either variable before.

Adds lib/providers.test.mjs -- 15 tests on Node's built-in runner, so the app
gains no dependency:

    node --test edgeLLM/nextjs-nemotron-app/lib/providers.test.mjs

Against this branch: 15 pass. Against lib/providers.js as it stands on main:
9 pass, 6 fail. Four of the six are behaviour --

    a redirected base URL does not receive the server key
    whitespace around a redirected URL does not slip the key through
    a redirected URL under an env-configured endpoint is still a redirect
    the custom backend withholds its key when pointed somewhere else

-- and two assert on `keyWithheld`/`serverBaseUrl`, which this change adds, so
they could not pass before it. The behavioural tests deliberately assert
nothing about those fields, so the four above fail on what the resolver does
rather than on a field that is missing. The 9 that pass either way are the
ordinary paths.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@egeboy35
egeboy35 force-pushed the fix/server-key-not-paired-with-caller-url branch from 78460e6 to f0e4ada Compare September 1, 2026 12:43
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