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/v1with no trailing slash, ansg-api-key, andprovider/modelids. You keep the OpenAI Python or Node.js SDK. - Chat first. Safeguard serves
POST /chat/completions,GET /models,GET /models/{id}andGET /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
nabove 1 are refused.logprobs,logit_bias,storeandmetadataare 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.
| Area | OpenAI's API (per OpenAI docs) | Safeguard Console (per Safeguard docs) |
|---|---|---|
| Key | OpenAI project key | sg-api- plus 64 lowercase hex characters, scope inference only |
| Model names | gpt-6-sol style | openai/gpt-6-sol style |
| Content | Text, images, audio by model | Text only in v1 |
| Rate limits | Organization and project level, several measures, x-ratelimit-* headers | Per key, requests per minute, default 60, up to 10,000 |
| Credit and spend limits | Reported as 429 codes | Per key and per workspace monthly limits, reported as 402 codes |
| Billing | Billed by OpenAI | Prepaid Safeguard credit, or your own provider key |
| Cost in response | Not included | usage.cost in US dollars and a provider field |
| Request id | x-request-id, exposed by the SDKs as _request_id | gen- id in the body and the X-Generation-Id header |
| Organizations and projects | Meaningful | No meaning; a key belongs to your workspace |
| Retries | SDKs retry some errors twice by default | Gateway 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:
- Chat Completions calls with text messages. These move.
- 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.
- Chat calls using features Safeguard does not take: image or audio parts,
ngreater 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
inferencescope covers chat completions, model listing andGET /key, nothing else. MCP keys and sign-in tokens are refused with401 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 send | On Safeguard |
|---|---|
Text messages | Same. Roles system, developer, user, assistant, tool; 1 to 2,000 messages |
| Image or audio parts | Refused, 400 unsupported_parameter |
max_tokens, max_completion_tokens | Same; max_completion_tokens wins if both; capped at the model maximum |
| No token cap | Gateway sends 4,096 or the model maximum, whichever is lower |
temperature, top_p, stop | Same ranges |
stream, stream_options.include_usage | Same |
tools, tool_choice, parallel_tool_calls, response_format, seed, penalties | Passed to the provider unchanged |
n greater than 1 | Refused, 400 unsupported_parameter |
user | Accepted and dropped, never sent to the provider |
logprobs, logit_bias, store, metadata, anything else | Ignored, not forwarded |
| Body over 8 MiB | Refused, 413 request_too_large |
Three rows cause most surprises:
- The default token cap. Code that never sets
max_tokensgets 4,096 or the model maximum, so long answers can end withfinish_reasonoflength. Set a cap on every call; it also shrinks the credit hold in Step 6. - Ignored fields fail late.
logprobsis dropped, not refused, so code readingchoices[0].logprobsfinds nothing far from the call. - Anthropic models take fewer parameters. For models served through Anthropic's API, only
temperature,toolsandtool_choiceare 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.
| Status | code | Meaning | Retry? |
|---|---|---|---|
| 400 | invalid_request, unsupported_parameter | Bad body, non-text part, n above 1, routing field where routing is off | No |
| 400 | upstream_rejected | The provider refused the request | No |
| 401 | invalid_api_key | Key missing, malformed, unknown or revoked | No |
| 402 | insufficient_credit | Credit does not cover the most the request could cost | After credit is added, or with lower max_tokens |
| 402 | key_spend_limit_exceeded, account_spend_limit_exceeded | Would pass the key or workspace monthly limit | After a limit change, or next month |
| 403 | gateway_disabled | Platform or endpoint not on for the workspace | No |
| 404 | model_not_found | Not in the catalog | No |
| 429 | rate_limited | Key's requests per minute reached | Yes, after Retry-After |
| 429 | upstream_rate_limited | Provider limiting the gateway; Retry-After is 5 | Yes |
| 500 | internal_error | Gateway failure | Once or twice, with backoff |
| 502 | upstream_error | Provider error, unreachable, or stream broke | Yes, with backoff |
| 502 | upstream_auth | Provider refused Safeguard's credentials or your provider key | No; try another model |
| 503 | model_unavailable | Not past launch checks, ZDR not offered, or catalog unreadable | Only the catalog case clears |
| 503 | auth_unavailable, billing_unavailable | Keys or credit could not be checked | Yes, after a short wait |
| 504 | upstream_timeout | Provider too slow, or stream past 900 seconds | Yes, 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.
| Situation | Suggested route |
|---|---|
| One prepaid balance and cost in every response | Safeguard credit, with cost reported per response |
| Negotiated terms or committed spend with a provider | Your own provider key for that route |
| Provider's enterprise data terms must govern prompts | Your own provider key plus that agreement |
| Trying a self-hosted model interactively | Your 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.
| Phase | What you do | Exit when |
|---|---|---|
| 0. Inventory | Bucket call sites, build the model map, create keys with limits | Every chat call is mapped; non-chat calls have an owner |
| 1. Contract tests | Run existing LLM tests against Safeguard with a staging key | Parameters you rely on behave as expected per model |
| 2. Parallel run | Send non-sensitive test prompts down both paths; compare output, finish_reason, usage.cost | Differences are explained |
| 3. Canary | Route a small, sticky share of production traffic behind a flag | Error codes, latency and cost within your bounds |
| 4. Ramp | Raise the share in steps, each held through a traffic peak | Full traffic, no open issues |
| 5. Cleanup | Remove the old path; keep OpenAI only for non-chat endpoints; rotate test keys | A 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:
- Create a new key with the same limits.
- Deploy it everywhere the old key is used.
- Confirm the old key is idle: its Last used time stops changing (updated about once a minute) and its This month spend stops growing.
- 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_URLglobally. Every SDK-based tool follows it, including ones calling endpoints Safeguard does not serve. - Bare model names.
gpt-6-solreturns404 model_not_found; copy the id, not the page URL. - Trusting the catalog listing. Only
runnable: truemeans 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 thegen-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 situation | What to use |
|---|---|
| A fixed version exists | Autonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version |
| You need to know today whether you are affected | Gold Open Source lookup and "Am I affected?" checker, free and without login |
| You want vulnerable versions to stop shipping | Safeguard policies and gates in CI |
| Upstream has no fix yet, or will not ship one | A custom Gold request; fork and patch |
| You want to start clean on new projects | Pull 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
- Safeguard docs: Migrate from OpenAI
- Safeguard docs: API Platform (Console) overview
- Safeguard docs: Quickstart
- Safeguard docs: Authentication
- Safeguard docs: SDKs and clients
- Safeguard docs: Chat completions
- Safeguard docs: Models
- Safeguard docs: Errors
- Safeguard docs: Embeddings
- Safeguard docs: Request lifecycle
- Safeguard docs: Streaming
- Safeguard docs: Routing
- Safeguard docs: Reliability and fallbacks
- Safeguard docs: Your own provider keys
- Safeguard docs: Bring your own model
- Safeguard docs: Key management best practices
- Safeguard docs: Spend controls
- Safeguard docs: Usage and analytics
- Safeguard docs: Privacy and data
- Safeguard docs: Zero data retention
- OpenAI: openai-python README
- OpenAI: openai-node README
- OpenAI docs: Migrate to the Responses API
- OpenAI docs: Error codes
- OpenAI docs: Rate limits
- OpenAI docs: Production best practices
- IETF RFC 9110: HTTP Semantics, Retry-After
- IETF RFC 6750: OAuth 2.0 Bearer Token Usage
- WHATWG HTML Standard: Server-sent events
- OWASP Gen AI Security Project: LLM10:2025 Unbounded Consumption
- OWASP Cheat Sheet Series: Secrets Management
- Google SRE Workbook: Canarying Releases
- NIST SP 800-57 Part 1 Rev. 5: Recommendation for Key Management
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.