DEV Community

Alper San
Alper San

Posted on • Originally published at salting.io

One Bridge for every OpenAI route

One SaltingIO Bridge can stand in for every OpenAI route you call. Here's how template variables turn five endpoints into a single, parameterised proxy.

Last month I was hooking SaltingIO up to a small chat UI that needed three OpenAI calls: chat completions, embeddings, and a moderation check before sending anything user-typed upstream. My first instinct was the obvious one. Three separate Bridges, three UUIDs, three fetches from the frontend.

It worked. It was also annoying.

Every time the team wanted a new OpenAI feature (assistants, vector stores, an admin call to list models), I was back in the dashboard creating another Bridge, copying another UUID into the codebase, redeploying. The credential never changed. Only the path did. That's not the shape of a problem that should require N records.

If you've felt the same itch, this post is about how template variables in a Bridge URL collapse those N records into one. I'll walk through the working setup, the curl shape, the trade-offs, and the one thing that bit me on the way.

The shape of a templated Bridge

A SaltingIO Bridge stores a destination URL plus headers, methods, and origin rules. The destination URL is the part most people leave static, but it accepts a templating syntax: {{name}} placeholders that get substituted at request time from query parameters.

So instead of pinning the Bridge to

https://api.openai.com/v1/chat/completions

, you point it at:

https://api.openai.com/v1/{{path}}

The Authorization header still carries your sk-... key, stored once on the Bridge. Allowed methods are POST and GET. Allowed origins are your two or three frontends.

From the browser:

const res = await fetch(
  `https://api.salting.io/r/${BRIDGE_UUID}?path=chat/completions`,
  {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      model: "gpt-4o-mini",
      messages: [{ role: "user", content: "say hi" }],
    }),
  },
);
Enter fullscreen mode Exit fullscreen mode

Same UUID, different path, totally different OpenAI route. The body is forwarded as-is. Your key never touches the browser.

Curl, in case you want to sanity-check from a terminal:

curl -X POST \
  "https://api.salting.io/r/$BRIDGE_UUID?path=embeddings" \
  -H 'Content-Type: application/json' \
  -d '{"model":"text-embedding-3-small","input":"hello"}'
Enter fullscreen mode Exit fullscreen mode

The placeholder is case-sensitive. {{path}} and ?path= line up. {{Path}} against ?path= will silently miss and the upstream call goes to /v1/{{Path}}, which is not a fun stack trace to read on a Friday afternoon.

Why one Bridge beats N Bridges

There's a maintenance argument and a security argument.

The maintenance side is small but real. One Bridge means one origin allowlist to keep in sync, one set of allowed headers, one place to rotate the OpenAI key when it inevitably leaks (everyone leaks one eventually). When you have five Bridges all pointing at OpenAI with the same key, rotating that key is five edits, and the chance you forget one scales linearly.

The security argument is more interesting. The fewer records in your account, the smaller your attack surface in the dashboard itself. If a Viewer on your team account gets a stale browser session, you'd rather they could enumerate one record than seven. SaltingIO Logs aggregate per-UUID, so a single Bridge also collapses your usage view into one row. You see total OpenAI spend per day on one chart instead of summing five.

The flip side: a templated Bridge is more permissive than a fixed one. The whole /v1/{{path}} family is now reachable from your frontend, including routes you didn't intend to expose. If the only thing you ever call is chat/completions, a static Bridge to chat/completions is a tighter fit. Templating widens the door so you don't have to keep cutting new ones.

Constraining the template

You can pull the door back in two ways.

First, allowed methods. If your real traffic is only POST, drop GET, PUT, PATCH, DELETE from the Bridge config. A leaked UUID with a method allowlist of just POST cannot, for example, drive DELETE /v1/files/{file_id} even with the right path value.

Second, validate on your side. Template variables are substitution, not validation. If you want to accept only chat/completions, embeddings, and moderations, do that check in your client code before the call goes out. SaltingIO won't reject path=admin/something-you-shouldn't-touch. It'll happily forward it.

For most apps, methods plus the origin allowlist are enough, and the wins from one Bridge per upstream are worth the wider path surface. But if you're handing a Bridge to a less-trusted internal team, lean toward a static URL. The point of {{path}} is consolidation for routes you already trust yourself to call.

Combining with response transformation

The other thing that pairs well with template variables is ?select=. SaltingIO can reshape the upstream JSON before it reaches the browser using a GJSON path.

OpenAI chat responses are chunky. The piece you usually want is choices.0.message.content. Adding &select=choices.0.message.content to the same Bridge call returns just that string, wrapped in the standard { "data": "..." } envelope:

curl -X POST \
  "https://api.salting.io/r/$BRIDGE_UUID?path=chat/completions&select=choices.0.message.content" \
  -H 'Content-Type: application/json' \
  -d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"hi"}]}'
Enter fullscreen mode Exit fullscreen mode

Two query params, one Bridge, no backend, no exposed key, no 4 KB of JSON the user agent has to parse just to grab a string. It's the kind of small win that adds up across a session.

Where this approach is the wrong tool

Templating is not a fit for every Bridge. A few honest caveats.

If your upstream is on multiple hostnames (you're calling both api.openai.com and api.anthropic.com from the same UI), one Bridge can't host both. The destination URL field is one host. Two upstreams means two Bridges. Don't try to template the host part to dodge that, it's a footgun.

If you need failover URLs (a primary plus backups tried in order), make sure the same {{placeholder}} shape exists in every entry. SaltingIO substitutes the same query parameters into the failover URL on retry, so a mismatch between primary and backup paths will silently send the failover request to a different route.

If you care about fine-grained per-route quotas, a single templated Bridge can't tell /embeddings traffic from /chat/completions traffic at the rate-limit layer. The Bridge sees one record, one budget. Split the routes if you need to budget them separately.

What I'd do tomorrow

If you're already on SaltingIO with two or three Bridges that share an upstream and a credential, try the consolidation. Create one templated Bridge, switch one frontend route over to it, watch Logs for a day, then migrate the rest. The rollback is one query param away.

If you haven't shipped your first Bridge yet, start with a templated one for whichever upstream you'll call most. Static is always a refactor away if you decide later you want a tighter fit.

Read the docs for the full template variable syntax, including header and body interpolation.

Top comments (0)