Safeguard
Product•

Move from the OpenAI API to Safeguard Console: a safe cutover

Moving Chat Completions code from OpenAI to Safeguard Console takes three changes: base URL, key and model id. Here is how to check the rest and cut over.

Safeguard Security Team
38 min read

Code written for OpenAI's Chat Completions API moves to Safeguard Console with three changes: point the client at https://api.safeguard.sh/gpt/v1, send an sg-api- key instead of your OpenAI key, and rename each model to its provider/model id, such as openai/gpt-6-sol. The rest of the work is checking parameters, endpoints, errors and limits, then moving traffic in stages.

This guide is for the AI platform owner who runs LLM features in production and the head of engineering who signs off on the switch. Safeguard Console, which the docs call the API Platform, is Safeguard's OpenAI-compatible inference API and its console: one key for models from several providers, billed from one prepaid balance, with per-key spend limits, a metadata-only request log and opt-in fallbacks. Facts below come from docs.safeguard.sh and OpenAI's own documentation. Where we recommend something the docs do not prescribe, we label it a suggestion.

Key takeaways

  • Three code changes. Base URL https://api.safeguard.sh/gpt/v1 with no trailing slash, an sg-api- key, and provider/model ids. You keep the OpenAI Python or Node.js SDK.
  • Chat first. Safeguard serves POST /chat/completions, GET /models, GET /models/{id} and GET /key. Responses, Assistants, images, audio, files, batches and moderations stay on OpenAI. Embeddings move only where Safeguard has switched that endpoint on for your workspace.
  • Text only, one choice. Image or audio parts and n above 1 are refused. logprobs, logit_bias, store and metadata are silently ignored.
  • Money errors are 402s. Credit and spend limits return HTTP 402, not 429.
  • Fallbacks are opt-in and stay within the same model family. Multi-model routing is switched on per workspace.
  • Your own provider keys run requests on your provider account at a recorded Safeguard cost of $0.
  • Cut over in stages behind a flag, and keep the OpenAI path warm until the new one has run clean.

What changes and what stays the same

The request body is OpenAI's Chat Completions format, the response has OpenAI's shape, and errors use OpenAI's {"error": {"message", "type", "code", "param"}} body, so the OpenAI SDKs raise their usual exceptions. What changes is everything around the call.

AreaOpenAI's API (per OpenAI docs)Safeguard Console (per Safeguard docs)
KeyOpenAI project keysg-api- plus 64 lowercase hex characters, scope inference only
Model namesgpt-6-sol styleopenai/gpt-6-sol style
ContentText, images, audio by modelText only in v1
Rate limitsOrganization and project level, several measures, x-ratelimit-* headersPer key, requests per minute, default 60, up to 10,000
Credit and spend limitsReported as 429 codesPer key and per workspace monthly limits, reported as 402 codes
BillingBilled by OpenAIPrepaid Safeguard credit, or your own provider key
Cost in responseNot includedusage.cost in US dollars and a provider field
Request idx-request-id, exposed by the SDKs as _request_idgen- id in the body and the X-Generation-Id header
Organizations and projectsMeaningfulNo meaning; a key belongs to your workspace
RetriesSDKs retry some errors twice by defaultGateway makes one attempt unless fallbacks are on

Two rows change code. OpenAI's error guide documents credit exhaustion and spend limits as 429s (credit_balance_exhausted, organization_spend_limit_exceeded, project_spend_limit_exceeded), while Safeguard returns 402. And the SDK's _request_id reads x-request-id, which on Safeguard is an internal audit id. The id Safeguard asks you to quote is the gen- id.

Before you start: inventory how your code calls OpenAI

Start with a list, not a code change. These commands are a suggestion for a typical repository.

# Where clients are created, and which SDK versions you run
rg -n "OpenAI\(|AsyncOpenAI\(|new OpenAI\(" --type py --type ts --type js
pip show openai 2>/dev/null | grep -i '^version'
npm ls openai 2>/dev/null

# Endpoints other than chat.completions need a plan
rg -n "\.responses\.|\.embeddings\.|\.images\.|\.audio\.|\.files\.|\.batches\.|\.moderations\.|beta\.assistants"

# Hard coded model names
rg -n "model\s*=\s*['\"]|model:\s*['\"]" --type py --type ts --type js

# Parameters that behave differently on Safeguard
rg -n "logprobs|logit_bias|store\s*=|metadata\s*=|\bn\s*=\s*[2-9]|image_url|input_audio"

# Environment variables that silently redirect SDK clients
rg -n "OPENAI_BASE_URL|OPENAI_API_KEY|OPENAI_ORG|OPENAI_PROJECT"

Sort what you find into three buckets:

  1. Chat Completions calls with text messages. These move.
  2. Calls to other OpenAI endpoints. OpenAI's migration guide recommends the Responses API for new projects and says Chat Completions remains supported. Code already on Responses, Assistants, images, audio, files, batches or moderations stays on OpenAI; Safeguard's migration guide says those paths get a 404. Embeddings are covered in Step 4.
  3. Chat calls using features Safeguard does not take: image or audio parts, n greater than 1, logprobs, logit_bias. These need a code change or stay on OpenAI.

Record client location, endpoint, model, parameters and bucket per service. That table becomes your cutover checklist.

Step 1: turn on the API Platform and create keys

Get the platform switched on

Safeguard switches the API Platform on per workspace. To request it, open a model at safeguard.sh/models, choose Use with Safeguard and leave your email address. Until it is on, Create key is disabled and requests are refused with 403 gateway_disabled. Requests spend your workspace's prepaid credit, so check the balance under Usage first.

Create one key per application and environment

In the web app, open the product switcher and choose API Platform (Console), or press G then 5. Open API keys, choose Create key, and fill in a Name (such as prod-checkout-service), an optional Monthly spend limit (USD) and an optional Requests per minute (default 60). The Copy your key dialog shows the full key once; the Escape key closes it and the key is gone, so copy it straight into your secret manager.

Facts that shape the migration:

  • Safeguard stores only a SHA-256 hash of the key, plus its start and last four characters for a masked display such as sg-api-1a2b3c4d…9f8e.
  • The inference scope covers chat completions, model listing and GET /key, nothing else. MCP keys and sign-in tokens are refused with 401 invalid_api_key.
  • A key belongs to the member who created it, spends workspace credit, and can call every model available to the workspace.
  • Console keys do not expire. They stop working when revoked, or when the owner is deactivated, deleted, suspended or locked.

That last point matters. If the person who created the production key leaves, the key stops. Suggestion: create production keys from a service owner account your team controls, and record each key's owner in the runbook.

One key per application and environment (production, staging, CI) means you can revoke one without touching the others, give each its own limits, and read spend per key in Usage, which is the view you want while comparing paths.

Store the key where your app reads secrets

The docs read the key from SAFEGUARD_INFERENCE_KEY. Avoid SAFEGUARD_API_KEY, which Safeguard's CI/CD scanning setup uses for a different kind of key. OpenAI's production guidance says the same about its own keys: use environment variables or a secret management service, not hard coded values, which is also the core of OWASP's Secrets Management Cheat Sheet.

# Kubernetes: reference a secret, never a literal value
env:
  - name: SAFEGUARD_INFERENCE_KEY
    valueFrom:
      secretKeyRef:
        name: llm-gateway
        key: safeguard-inference-key

The key is a bearer credential, sent as an Authorization: Bearer header in the style RFC 6750 defines: whoever holds it can use it. Never put it in browser code, a mobile app or a desktop app you distribute. Call the API from your server, redact the Authorization header in request logs, and quote a key's name or masked form, never the key, when you report a problem.

Step 2: swap the base URL

Python

import os
from openai import OpenAI

# Before: client = OpenAI()  # OpenAI's API, OPENAI_API_KEY
client = OpenAI(
    base_url="https://api.safeguard.sh/gpt/v1",
    api_key=os.environ["SAFEGUARD_INFERENCE_KEY"],
)

response = client.chat.completions.create(
    model="openai/gpt-6-sol",  # was "gpt-6-sol"
    messages=[{"role": "user", "content": "List three ways to reduce dependency risk."}],
    max_tokens=300,
)
print(response.choices[0].message.content)
print("served by", response.provider, "cost USD", response.usage.cost)
print("request id", response.id)  # gen-..., quote this to Safeguard

The Python SDK keeps fields it does not know, so provider and usage.cost read as attributes.

Node.js and TypeScript

import OpenAI from 'openai';

const client = new OpenAI({
  baseURL: 'https://api.safeguard.sh/gpt/v1',
  apiKey: process.env.SAFEGUARD_INFERENCE_KEY,
});

const response = await client.chat.completions.create({
  model: 'openai/gpt-6-sol',
  messages: [{ role: 'user', content: 'List three ways to reduce dependency risk.' }],
  max_tokens: 300,
});
console.log(response.provider, response.usage.cost);

In TypeScript these two fields are not in the SDK's types, so the docs suggest a cast such as (response.usage as { cost?: number }).cost.

Environment variables only

Both SDKs read OPENAI_BASE_URL and OPENAI_API_KEY when a client is created without arguments, which helps with tools you cannot edit.

export OPENAI_BASE_URL="https://api.safeguard.sh/gpt/v1"
export OPENAI_API_KEY="sg-api-..."

Set them only for the process that should use Safeguard. In a shell profile or a shared base image they redirect every SDK-based tool on the machine.

A client factory you can flip (suggestion)

Put client creation behind one function. It gives you the switch for a staged cutover and a one-line rollback.

# llm_client.py  (suggestion)
import os
from openai import OpenAI

def make_client(route: str) -> OpenAI:
    if route == "safeguard":
        return OpenAI(
            base_url="https://api.safeguard.sh/gpt/v1",
            api_key=os.environ["SAFEGUARD_INFERENCE_KEY"],
            max_retries=0,   # retry by error code ourselves, see Step 5
            timeout=330.0,   # longer than the gateway's 120 s provider wait; size it, see Step 5
        )
    return OpenAI(api_key=os.environ["OPENAI_API_KEY"])

Step 3: map the model ids

Safeguard's ids name the provider first: gpt-6-sol becomes openai/gpt-6-sol. Use the id shown on the model's page at safeguard.sh/models, not the page address. The docs' example: the page for openai/gpt-6-astra lives at /models/openai-gpt-6-astra, but the API takes openai/gpt-6-astra.

Confirm each model is callable

A catalog model is callable only after Safeguard's launch checks: listed as available with input and output prices; a provider account configured; for third parties, terms that allow resale, a Trust Center subprocessor listing, and DPA, privacy notice and AI disclosure coverage; and a successful real end-to-end call on that route.

# What your key can call now
curl -s https://api.safeguard.sh/gpt/v1/models \
  -H "Authorization: Bearer $SAFEGUARD_INFERENCE_KEY" | jq -r '.data[].id'

# Everything published, with why a model is not callable
curl -s "https://api.safeguard.sh/gpt/v1/models?all=true" \
  -H "Authorization: Bearer $SAFEGUARD_INFERENCE_KEY" \
  | jq -r '.data[] | [.id, .status, (.runnable|tostring), (.zdr_available|tostring), (.unavailable_reason // "")] | @tsv'

Read runnable (true only when callable now), status, unavailable_reason, max_output_tokens (requests are capped at it), the per-million-token prices and zdr_available. GET /models/{id} takes the id with its slash, and an unknown id returns 404 model_not_found. The catalog also lists embedding, image, rerank and transcription models the chat endpoint does not serve, and a model shown as Price on request cannot be called.

Keep the mapping in configuration and test it (suggestion)

Hard coded names are why a swap works in one service and fails in another. A mapping file plus a CI check catches that.

# model_map.py
MODEL_MAP = {"gpt-6-sol": "openai/gpt-6-sol", "gpt-6-astra": "openai/gpt-6-astra"}

def to_safeguard(model: str) -> str:
    return MODEL_MAP.get(model, model if "/" in model else f"openai/{model}")
# tests/test_model_map.py: fails CI when a mapped model is not callable
import os, httpx
from model_map import MODEL_MAP

def test_every_mapped_model_is_runnable():
    r = httpx.get("https://api.safeguard.sh/gpt/v1/models",
                  headers={"Authorization": f"Bearer {os.environ['SAFEGUARD_INFERENCE_KEY']}"},
                  timeout=30)
    r.raise_for_status()
    callable_ids = {m["id"] for m in r.json()["data"]}
    missing = sorted(set(MODEL_MAP.values()) - callable_ids)
    assert not missing, f"not callable through Safeguard: {missing}"

Step 4: check every parameter and endpoint you send

The gateway reads and validates some fields, passes a fixed set to the provider unchanged, and ignores everything else without forwarding it.

You sendOn Safeguard
Text messagesSame. Roles system, developer, user, assistant, tool; 1 to 2,000 messages
Image or audio partsRefused, 400 unsupported_parameter
max_tokens, max_completion_tokensSame; max_completion_tokens wins if both; capped at the model maximum
No token capGateway sends 4,096 or the model maximum, whichever is lower
temperature, top_p, stopSame ranges
stream, stream_options.include_usageSame
tools, tool_choice, parallel_tool_calls, response_format, seed, penaltiesPassed to the provider unchanged
n greater than 1Refused, 400 unsupported_parameter
userAccepted and dropped, never sent to the provider
logprobs, logit_bias, store, metadata, anything elseIgnored, not forwarded
Body over 8 MiBRefused, 413 request_too_large

Three rows cause most surprises:

  • The default token cap. Code that never sets max_tokens gets 4,096 or the model maximum, so long answers can end with finish_reason of length. Set a cap on every call; it also shrinks the credit hold in Step 6.
  • Ignored fields fail late. logprobs is dropped, not refused, so code reading choices[0].logprobs finds nothing far from the call.
  • Anthropic models take fewer parameters. For models served through Anthropic's API, only temperature, tools and tool_choice are sent from the optional fields. Test per model if you plan to mix providers.

When a provider rejects a parameter, the gateway adjusts and resends to the same provider: it switches between max_tokens and max_completion_tokens, or drops an unsupported optional parameter. These are not fallbacks.

provider is Safeguard's extension, so the SDKs send it through extra_body in Python (documented in the OpenAI SDK README for this purpose) or as an extra field in Node.js. Older SDK versions may reject newer fields such as stream_options; upgrade or use extra_body.

response = client.chat.completions.create(
    model="openai/gpt-6-sol",
    messages=[{"role": "user", "content": "Summarise this incident report in three bullets: ..."}],
    max_tokens=400,
    extra_body={"provider": {"zdr": True, "allow_fallbacks": True}},
)

Endpoints that stay on OpenAI, and the embeddings question

Keep two clients, one per provider; the factory already supports it. Do not set OPENAI_BASE_URL globally and hope. A path Safeguard does not serve returns 404 from its router with {"detail": "Not Found"}, which your OpenAI error handling will not expect.

Embeddings need their own decision. The migration guide lists them among calls to keep on OpenAI, and the Console overview describes chat completions and models only. The docs also include a POST /embeddings reference in OpenAI's Embeddings format that Safeguard switches on per workspace; where it is off, it answers 403 gateway_disabled. Ask Safeguard whether it is on for you before you plan around it.

If you move embeddings, the docs give the key constraint: a request uses exactly the model you name, with no routing and no fallbacks, because vectors from different models cannot be compared. Suggestion: keep the same embedding model on both sides, or plan a full re-index. Mixing two models' vectors in one index degrades search without raising an error.

Step 5: errors, retries and timeouts

The codes your handler must learn

code is the stable field to branch on; message wording may change.

StatuscodeMeaningRetry?
400invalid_request, unsupported_parameterBad body, non-text part, n above 1, routing field where routing is offNo
400upstream_rejectedThe provider refused the requestNo
401invalid_api_keyKey missing, malformed, unknown or revokedNo
402insufficient_creditCredit does not cover the most the request could costAfter credit is added, or with lower max_tokens
402key_spend_limit_exceeded, account_spend_limit_exceededWould pass the key or workspace monthly limitAfter a limit change, or next month
403gateway_disabledPlatform or endpoint not on for the workspaceNo
404model_not_foundNot in the catalogNo
429rate_limitedKey's requests per minute reachedYes, after Retry-After
429upstream_rate_limitedProvider limiting the gateway; Retry-After is 5Yes
500internal_errorGateway failureOnce or twice, with backoff
502upstream_errorProvider error, unreachable, or stream brokeYes, with backoff
502upstream_authProvider refused Safeguard's credentials or your provider keyNo; try another model
503model_unavailableNot past launch checks, ZDR not offered, or catalog unreadableOnly the catalog case clears
503auth_unavailable, billing_unavailableKeys or credit could not be checkedYes, after a short wait
504upstream_timeoutProvider too slow, or stream past 900 secondsYes, with backoff

Checks run in a fixed order: key, platform switch, body, rate limit, model, credit and limits, then the provider call. A request refused in the first five never reaches a provider and costs nothing. If your handler treats every 429 as "back off", it keeps working for Safeguard's rate limits; special handling for OpenAI's credit and spend 429s must move to the 402 branch, and a 402 should alert a person rather than retry.

Do not run two retry loops

The OpenAI SDK READMEs say connection errors, 408, 409, 429 and 500-plus responses are retried twice by default. Against Safeguard that also retries 503 model_unavailable, which mostly does not clear, and with fallbacks on the gateway already retries once. The docs ask you not to run two loops. Suggestion: set max_retries=0 (Python) or maxRetries: 0 (Node.js) on the Safeguard client and retry by code, as in the docs' example:

import os, random, time
import httpx

URL = "https://api.safeguard.sh/gpt/v1/chat/completions"
RETRY = {"rate_limited", "upstream_rate_limited", "upstream_error", "upstream_timeout",
         "auth_unavailable", "billing_unavailable", "internal_error"}

def create(body, attempts=4):
    headers = {"Authorization": f"Bearer {os.environ['SAFEGUARD_INFERENCE_KEY']}"}
    for attempt in range(attempts):
        r = httpx.post(URL, json=body, headers=headers, timeout=300)
        if r.status_code < 400:
            return r.json()
        try:
            code = r.json()["error"]["code"]
        except (ValueError, KeyError, TypeError):
            code = None
        if code not in RETRY or attempt == attempts - 1:
            r.raise_for_status()
        wait = r.headers.get("Retry-After")
        time.sleep(int(wait) if wait else 2 ** attempt + random.random())

Retry-After is the standard HTTP header from RFC 9110 that tells a client how long to wait. For rate_limited it is the time until the oldest counted request leaves the 60 second window, and refused requests are not counted, so a retry after the wait goes through. Each retry is a new request; one that reaches a provider and gets an answer is billed.

Timeouts and request ids

The gateway waits up to 120 seconds for a provider by default (never more than 600), and a stream can run up to 900 seconds. With fallbacks on, one request can make several attempts, each with its own timeout. The SDK READMEs give a 10 minute default timeout with two retries. Size the Safeguard client's timeout to the attempts you allow, and stream long answers.

Log response.id (gen- plus 32 hex characters), also sent as X-Generation-Id. X-Request-Id is Safeguard's internal audit id, so the SDK's _request_id is not the id to quote. Refusals before the provider call, such as 401, 402 or rate_limited, carry no request id.

Step 6: limits, spend and the credit hold

Before calling a provider, the gateway holds the most the request could cost: a deliberately high estimate of input tokens plus every allowed output token, at the price of the most expensive model it might use. The hold must fit your credit, then the key's monthly limit, then the workspace's, or the answer is a 402 naming which. After the call it charges the actual cost, computed as input tokens times input price plus output tokens times output price, divided by one million. A request that errors before any output costs nothing.

So a request can be refused with a little credit left, and concurrent long requests each hold their own amount. A smaller max_tokens makes the hold smaller.

Set limits before the first production request. Key limits are set at creation or with Edit and reach the gateway within 30 seconds. A tenant admin or super admin sets the workspace limit under Usage, Credits and limits, Gateway settings. Months are UTC calendar months. The requests per minute limit is separate, and the docs describe it as a guard against runaway clients, not a spend cap. Size each production key's limit to real peak traffic, or the default of 60 will throttle a busy service on day one.

OWASP's Top 10 for LLM Applications names unbounded consumption as a risk: uncontrolled inference that disrupts service or drains an organization's money. Per-key limits, a workspace ceiling and max_tokens on every call are the Console's controls for it.

GET /key returns the calling key's name, prefix, scopes, limits, this month's spend and the workspace's available credit. A startup health check against it catches a wrong key (401), a workspace that is not on (403) and an exhausted budget before a user does. Spend and credit fields are omitted when billing cannot answer, so treat them as optional.

curl -s https://api.safeguard.sh/gpt/v1/key -H "Authorization: Bearer $SAFEGUARD_INFERENCE_KEY" | jq .

Routing and fallbacks: keep traffic up when a provider fails

The default: one attempt

By default the gateway calls the requested model's provider once and returns what happens. It does not retry or switch models, and the provider SDK's own retries are off. The only exception is the parameter adjustment from Step 4.

Opt-in retries and same-family fallbacks

A tenant admin or super admin can turn on Allow retries and fallbacks under Gateway settings, and a request can send provider.allow_fallbacks, which overrides the workspace setting either way.

response = client.chat.completions.create(
    model="openai/gpt-6-sol",
    messages=[{"role": "user", "content": "Hello"}],
    max_tokens=200,
    extra_body={"provider": {"allow_fallbacks": True}},
)
print(response.model, response.provider)  # what actually answered, and the route

With them on, a server error, timeout, connection failure or provider rate limit gets one retry on the same model, then the model's listed fallbacks in order, each with one retry. A credentials refusal skips straight to the next fallback. An upstream_rejected stops everything; that request is never retried.

The rules that make this safe:

  • Safeguard lists each model's fallbacks in the catalog; you cannot choose them.
  • A fallback is only the same model family (same maker) or the same model on another host.
  • It must be callable in its own right and, under zero data retention, offer it too.
  • It is billed at its own price; the hold covers the most expensive candidate.
  • Requests never fall back to the providers behind Safeguard's own AI features. The gateway uses separate credentials, and the docs say automated tests keep the paths apart.

For streams, retries and fallbacks happen only before the first chunk. The gateway never switches models mid-answer.

Multi-model routing

Where Safeguard has switched routing on for your workspace, a request can list candidates and steer providers:

{
  "model": "anthropic/claude-sonnet-5-5",
  "models": ["openai/gpt-6-sol", "google/gemini-3.5-flash"],
  "provider": {
    "order": ["anthropic", "openai"],
    "only": ["anthropic", "openai", "google"],
    "ignore": ["groq"],
    "sort": "price",
    "allow_fallbacks": true
  },
  "messages": [{ "role": "user", "content": "Hello" }]
}

models adds candidates after model, in your order. order prefers routes, only and ignore filter them, and sort orders candidates by price (estimated cost for this request at catalog price), latency (median end-to-end latency of successful gateway requests over the last 24 hours) or uptime (share of requests over the last 24 hours that did not fail at the provider). These are measured only from real gateway traffic, so a new model sorts last until it has been used.

Where routing is off, any routing field is refused with 400 unsupported_parameter and the field named in param. The gateway never ignores a routing field silently, so a missing fallback list fails loudly in testing rather than quietly in production. The X-Route header names the route that served the request.

Using it in a cutover (suggestion)

Leave fallbacks off during the canary so every failure is a real signal about the model you chose. Afterwards decide per workload: interactive features usually benefit from allow_fallbacks, while evaluation jobs and anything that must be reproducible should pin one model. Put the decision in the client factory, not in each call.

Bring your own provider keys

Many teams already hold a provider contract and want to keep it. The Console supports this in two ways that work differently.

Provider keys for catalog models

Safeguard switches provider keys on per workspace, in addition to the platform. Where they are off, the panel is hidden and the API answers 403 feature_disabled. Where on, a tenant admin or super admin sees Provider keys below the key list, chooses Set for a provider, pastes the key and saves. Routes that take your key: openai, anthropic, google, mistral, deepseek, xai, cohere, together, fireworks and groq.

Per the docs:

  • After saving, the key is never shown again; the Console shows its last four characters.
  • It sits in Safeguard's vault as a sealed credential that no person, workspace member or Safeguard administrator can list or reveal. Only the gateway reads it, when calling the provider for your workspace.
  • Replace overwrites it, Delete removes it, and switching it off sends requests back to Safeguard's access where offered.

Every request for that route, from an API key or the Workbench, then runs on your key: recorded cost $0, no credit reserved, marked in the request log, and governed by the provider's terms, limits and data handling under your account. Your Safeguard key, rate limits, routing and request log work as before.

If the provider refuses your key, the request fails with 502 upstream_auth and "Your provider key was refused by the provider." The gateway never falls back to Safeguard's access, so a request you meant to pay for directly is never charged to your credit.

Models you add yourself

A tenant admin can also add a model from your provider account or a self-hosted endpoint under Integrations, Configure Scanner, Models. The docs say these custom: models cannot be called with an API key: they are refused with 400 unsupported_parameter when called with an API key, and retries, fallbacks and zero data retention do not apply. For production API traffic, provider keys are the route. One docs conflict to know about: the Bring your own model page still says a catalog model always runs on Safeguard's account, while the provider keys page describes your key running catalog models. Confirm with Safeguard that provider keys are on for your workspace before you plan around them.

SituationSuggested route
One prepaid balance and cost in every responseSafeguard credit, with cost reported per response
Negotiated terms or committed spend with a providerYour own provider key for that route
Provider's enterprise data terms must govern promptsYour own provider key plus that agreement
Trying a self-hosted model interactivelyYour own model in the Workbench

For running Safeguard's own platform features with your model or none, see Zero AI and bring your own model.

Data handling: what changes for your prompts

Prompts still go to a model provider, chosen per request by the model you name. For a closed model that is its maker; for an open-weight model, its host. Each model page on safeguard.sh/models states where prompts go under Data policy. The gateway never forwards the user field or your Safeguard key, reads none of your workspace's security data, and does not pass through the provider chain behind Safeguard's own features.

Safeguard keeps a metadata-only log row per request that reaches a provider (time, source, key name and mask, model, provider, tokens, cost, latency, status, and whether it streamed, used zero data retention or was served by a fallback), never the text. Prompt and output text is stored only if an admin turns on Store prompts and outputs, which is off by default; Delete stored prompts removes it. Keys are kept only as SHA-256 hashes. Safeguard never uses prompts, outputs or logs to train models; a provider's own terms govern what it may do.

Zero data retention is a routing rule. Send "provider": {"zdr": true} or have an admin require it for the workspace, and the gateway uses only models with zdr_available: true, answers 503 model_unavailable for a model that does not offer it (unless fallbacks are on and an allowed fallback does), and sends store: false to OpenAI. A request cannot turn off a workspace requirement. The docs advise confirming the arrangement with Safeguard before sending regulated data.

Safeguard is SOC 2 Type I and Type II audited. Restricted documents such as audit reports are released through Request access on the Trust Center, and the docs advise checking that a report's scope covers the API Platform before you rely on it. The Trust Center also lists subprocessors, where providers appear once they pass the launch checks.

The staged cutover

A base URL swap is a deploy. Google's SRE workbook defines canarying as a partial, time-limited deployment of a change and its evaluation, used to decide whether to proceed. The phases below apply that to LLM traffic; they are suggestions built on documented behaviour.

PhaseWhat you doExit when
0. InventoryBucket call sites, build the model map, create keys with limitsEvery chat call is mapped; non-chat calls have an owner
1. Contract testsRun existing LLM tests against Safeguard with a staging keyParameters you rely on behave as expected per model
2. Parallel runSend non-sensitive test prompts down both paths; compare output, finish_reason, usage.costDifferences are explained
3. CanaryRoute a small, sticky share of production traffic behind a flagError codes, latency and cost within your bounds
4. RampRaise the share in steps, each held through a traffic peakFull traffic, no open issues
5. CleanupRemove the old path; keep OpenAI only for non-chat endpoints; rotate test keysA full billing cycle runs clean

In the parallel run, use the same model, the same parameters and an explicit max_tokens on both sides, or you measure the default cap rather than the gateway. Compare usage.cost with the catalog prices for the tokens used. Do not mirror prompts holding personal or regulated data into tests unless your policy allows it.

A sticky canary with a kill switch (suggestion)

Hashing a stable id keeps each user on one path for a whole conversation.

# canary.py
import hashlib, os
from llm_client import make_client
from my_logging import log_llm_call  # your own logging helper
from model_map import to_safeguard

CANARY_SHARE = float(os.environ.get("SAFEGUARD_CANARY_SHARE", "0.01"))
KILL_SWITCH = os.environ.get("SAFEGUARD_KILL_SWITCH") == "1"
clients = {"safeguard": make_client("safeguard"), "openai": make_client("openai")}

def route_for(stable_id: str) -> str:
    if KILL_SWITCH:
        return "openai"
    bucket = int(hashlib.sha256(stable_id.encode()).hexdigest()[:8], 16) / 0xFFFFFFFF
    return "safeguard" if bucket < CANARY_SHARE else "openai"

def chat(stable_id: str, model: str, messages, **kw):
    route = route_for(stable_id)
    m = to_safeguard(model) if route == "safeguard" else model
    resp = clients[route].chat.completions.create(model=m, messages=messages, **kw)
    log_llm_call(route=route, model=resp.model, request_id=resp.id,
                 cost=getattr(resp.usage, "cost", None))
    return resp

What to watch, and where

Watch two places. The Console's request log holds only requests that reached a provider; the docs say refusals before that (bad key, platform off, unknown or unavailable model, credit, spend or rate limit, invalid body) are not logged. Your client is the only place those appear, so count responses by status and error.code per route. The Console's Usage page gives totals, By model and By key tables, and the request log with status values such as upstream_error, upstream_timeout and client_disconnected.

Alert during the canary (suggestion) on any 401, 403 or 404 (configuration, not load), any 402 (a limit too low or holds larger than expected), more than a trickle of rate_limited, a rise in finish_reason of length (a missing max_tokens), and any 503 model_unavailable (check GET /models?all=true).

Rollback is the kill switch. Keep the OpenAI key valid and its code path deployed until Phase 5 is done.

Streaming during and after the move

Set "stream": true and read server-sent events, the WHATWG format of one data: line per chunk, ending with data: [DONE]. Add "stream_options": {"include_usage": true} for a final chunk with empty choices and a usage object including cost.

stream = client.chat.completions.create(
    model="openai/gpt-6-sol",
    messages=[{"role": "user", "content": "Write a haiku about dependency updates."}],
    max_tokens=100, stream=True, stream_options={"include_usage": True},
)
for chunk in stream:
    if chunk.choices:
        print(chunk.choices[0].delta.content or "", end="")
    elif getattr(chunk, "usage", None):
        print("\ncost USD", chunk.usage.cost)

Two behaviours to plan for. After the first chunk the status is already 200, so a provider failure arrives as an error event (upstream_error, or upstream_timeout past 900 seconds), then usage if requested, then [DONE]; the SDKs raise on it, and tokens produced so far are charged. And cancelling does not stop the charge: the gateway keeps reading the provider's answer to the end, up to 900 seconds, bills all of it and logs client_disconnected. Cap output with max_tokens.

Key rotation and leak response after the move

The Console does not rotate keys for you, but the docs describe a zero-downtime rotation:

  1. Create a new key with the same limits.
  2. Deploy it everywhere the old key is used.
  3. Confirm the old key is idle: its Last used time stops changing (updated about once a minute) and its This month spend stops growing.
  4. Revoke the old key.

Rotate on your policy's schedule, when someone with access leaves, and after suspected exposure. NIST SP 800-57 frames this as a cryptoperiod, the time a key is authorized for use, and OpenAI's production guidance recommends the same create, update, verify, revoke pattern for its keys. For automating the deploy step, see our API key rotation automation guide.

If a key leaks: revoke it at once (requests are refused within 30 seconds, because the gateway trusts a checked key for up to 30 seconds; in-flight requests finish), deploy a replacement, check its spend under Revoked and in Usage By key, and close the exposure. Our post on OpenAI API key leakage on GitHub covers how keys usually escape. For usage your team did not make, the docs say to contact hi@safeguard.sh with the key's name and time window. Deactivating a member stops all their keys at once, and reactivation does not restore them.

Common mistakes

  • Exporting OPENAI_BASE_URL globally. Every SDK-based tool follows it, including ones calling endpoints Safeguard does not serve.
  • Bare model names. gpt-6-sol returns 404 model_not_found; copy the id, not the page URL.
  • Trusting the catalog listing. Only runnable: true means callable now.
  • No max_tokens. You get the 4,096 default and a larger hold.
  • Two retry loops. SDK retries plus your own multiply attempts on codes that will not clear.
  • Retrying a 402. It is a budget decision; alert a person.
  • Logging _request_id. Quote the gen- id.
  • Watching only the Console. Pre-provider refusals are not in the request log.
  • Default 60 requests per minute on a busy key. Size it before cutover.
  • Production keys from a personal account. Deactivating that person stops the key.
  • Expecting cancel to save money. Cap output instead.

FAQ

Is Safeguard Console compatible with the OpenAI SDK?

Yes, for Chat Completions. Set the base URL to https://api.safeguard.sh/gpt/v1 and the key to your sg-api- key in the OpenAI Python or Node.js SDK. There is no Safeguard SDK to install.

How do I convert OpenAI model names to Safeguard model ids?

Prefix the provider: gpt-6-sol becomes openai/gpt-6-sol. Confirm the id on the model's page or with GET /models, and check runnable before switching traffic.

Can I use the Responses API or embeddings through Safeguard Console?

Keep Responses, Assistants, images, audio, files, batches and moderations on OpenAI. Embeddings have a POST /embeddings endpoint that Safeguard switches on per workspace; if you move them, keep the same embedding model or re-index.

What happens when Safeguard credit runs out?

Requests are refused with HTTP 402 insufficient_credit instead of being billed later. Key and workspace monthly limits return key_spend_limit_exceeded and account_spend_limit_exceeded. Refused requests cost nothing.

Does Safeguard Console fall back to another model automatically?

Not by default. A tenant admin can turn on retries and fallbacks for the workspace, or a request can send provider.allow_fallbacks. Fallbacks stay within the same model family and are listed by Safeguard.

Can we keep our own OpenAI contract and key?

Where provider keys are on for your workspace, an admin can set your key for the openai route and others. Those requests are billed by the provider and recorded at $0 in Safeguard, and a refused key fails rather than falling back to Safeguard's access.

Does Safeguard store or train on our prompts?

The request log holds metadata only. Text is stored only if an admin turns on Store prompts and outputs, off by default. Safeguard never trains on prompts, outputs or logs.

How fast does revoking a Safeguard key take effect?

Within 30 seconds. Limit changes on a key also reach the gateway within 30 seconds.

Ship the migration on dependencies you can vouch for

This migration touches your dependency tree: upgrading the OpenAI SDK for max_completion_tokens and stream_options pulls new versions of its HTTP client and other transitive packages into a service that holds a key able to spend money. The gateway does not change what is inside your build, so the same supply chain hygiene applies to the code that calls it.

Fix it without waiting for upstream

Whatever the vulnerability, most of the risk sits in two gaps. The first is the time between a fix being published and that fix actually running in your production builds. The second, and worse, is the time before any fix exists at all, while a maintainer works out a patch and a release schedule. Waiting through both gaps is a habit, not a requirement. Safeguard gives teams three ways to close them.

1. Autonomous remediation: pull requests, not tickets

When a fixed version exists, Griffin, Safeguard's security model, opens a policy gated pull request that moves the affected dependency to the safe version, and your own CI runs your tests on it before anyone merges. You can run it on one repository or in bulk across every project that carries the vulnerable version. Remediation categories include dependencies, licenses, security posture and findings, and each one is set separately. Reachability analysis ranks the vulnerable code paths your application actually calls first, so the first pull requests you merge are the ones that matter most. Auto-merge is a separate switch and stays off by default, so your team decides what merges on its own and what still needs a human approval.

2. Gold Open Source: the safe version in seconds, free

gold.safeguard.sh is free and needs no login. Search any CVE or package to see affected and fixed versions, CVSS, EPSS, CISA KEV status and a recommended safe version, or use the "Am I affected?" checker. The same data is available through a free read API, README status badges and RSS threat feeds.

3. Gold Registry: start from components that are already fixed

Safeguard Gold rebuilds patched versions of open-source packages and images from source, signs each build, attaches an SBOM, SLSA provenance and VEX, and serves them from registry.safeguard.sh to your organization. Every Gold artifact is held continuously to zero exploitable critical or high CVEs and a completed license check, with signed build provenance (signed with Safeguard's key and verifiable with cosign, SLSA Build Level 3 as the design target) and a public evidence page you can hand to an auditor. An artifact loses its Gold badge if it regresses. Teams can point their own package directory at the registry so hardened equivalents resolve without changing how developers install anything.

When upstream has no fix yet, or will not ship one, Safeguard first works to get the fix accepted upstream. If the issue is severe or upstream will not move, Safeguard builds a fork, fixes the vulnerability there and publishes a hardened Gold build, so you are not stuck on a maintainer's release schedule. You can also request a hardened build of a specific package. Ask which packages it covers before you plan around it.

Your situationWhat to use
A fixed version existsAutonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version
You need to know today whether you are affectedGold Open Source lookup and "Am I affected?" checker, free and without login
You want vulnerable versions to stop shippingSafeguard policies and gates in CI
Upstream has no fix yet, or will not ship oneA custom Gold request; fork and patch
You want to start clean on new projectsPull hardened components and images from Gold Registry

One honest limit: when the vulnerable code lives in a vendor appliance, a hosted service or an operating system runtime you do not build yourself, the fix still has to come from that vendor or distribution. The steps above apply to the open source dependencies and container images you build and ship, which is where most teams carry the largest and least tracked share of their exposure.

See how Safeguard would help

The Console is configured in the web app and called from your code, so these demos show the surfaces you are likely to use beside it. The docs draw the line this way: call models through the inference API when your application needs a model, and connect an assistant to Safeguard's MCP server when the assistant needs Safeguard's data.

The first demo connects Claude to the Safeguard MCP server with OAuth and shows how you choose which tools Claude can use. It is the other half of the API versus MCP decision, and the same least-privilege idea as one inference key per application.

Click through it at your own pace, or open it full screen.

The second demo is a real Safeguard CLI session. The assistant reads a finding and its advisory, asks before any command that changes something, edits a dependency version, runs the install and tests with approval and pushes nothing. That is the flow you would use to upgrade the OpenAI SDK and its transitive packages as part of this migration.

Click through it at your own pace, or open it full screen.

The third demo sets up the local runner on your own machine, approves it with its device code, then scans from the terminal. A scan of the service before and after the SDK upgrade shows what the dependency change added.

Click through it at your own pace, or open it full screen.

To try Safeguard yourself, download the apps or start with the CLI documentation. For the Console, begin with the migration guide in the docs.

Quick check

Your service calls client.images.generate and client.chat.completions.create. What do you change when moving to Safeguard Console?

Point a Safeguard client at the chat completions calls only: new base URL, an sg-api- key and provider/model ids. Keep a separate OpenAI client for the images call, because Safeguard does not serve that endpoint.

During the canary, a key starts getting HTTP 402 with key_spend_limit_exceeded. Should your client retry?

No. The request would take the key past its monthly spend limit, and retrying unchanged keeps failing. Raise or clear the key's limit on API keys (it applies within 30 seconds), use another key, or wait for the next UTC month.

Sources

Last reviewed: 5 November 2026. Console behaviour and limits are taken from docs.safeguard.sh; check the linked pages for changes before you plan a cutover.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

We email you a link to confirm, and only then add you to the list. Unsubscribe in one click. Privacy

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.