A small Node.js server that sits in front of an OpenAI-compatible chat API and
forces every response through a safety classifier. You talk to it like it's
OpenAI, but the only model it exposes is safely-v1.0, and what you get back is
a strict JSON safety verdict on your input — not an answer.
POST /v1/chat/completions → {"is_unsafe": true, "reason": "...", "danger_score": 9, ...}
-
You start the server. It generates a
sk-...API key, prints it once in the console, and stores an AES-256-GCM encrypted copy inencryption.keyandapi.keys.json. -
A client sends a normal OpenAI
chat.completionsrequest withmodel: "safely-v1.0". -
The server grabs the last user message, prepends
prompts/main.mdas a system prompt, and calls the upstream model you configured. -
The upstream's reply is parsed and validated against this schema:
{ "reasoning": "one-sentence analysis", "is_unsafe": false, "reason": "none", "danger_score": 0 }If the reply doesn't match, the server nags the model and tries again, up to 5 times. Only a valid response gets returned to the client.
-
The OpenAI-shaped response has the verdict as
choices[0].message.content(as a JSON string), so it works with the official OpenAI SDK unchanged.
git clone <this repo>
cd fireai-safely
npm install
cp config.json.example config.jsonEdit config.json:
{
"port": 8080,
"openai": {
"url": "https://api.openai.com/v1/chat/completions",
"id": "gpt-4o-mini",
"key": "sk-your-real-key"
}
}openai.url can point at any OpenAI-compatible endpoint, not just OpenAI.
Then:
npm startFirst run looks like this:
_____ _ _ ___ ____ __ _
| ___(_)_ __ ___ / \ |_ _| / ___| __ _ / _| ___| |_ _
| |_ | | '__/ _ \ / _ \ | | \___ \ / _` | |_ / _ \ | | | |
| _| | | | | __// ___ \ | | ___) | (_| | _| __/ | |_| |
|_| |_|_| \___/_/ \_\___| |____/ \__,_|_| \___|_|\__, |
|___/
API key generated
────────────────────────────────────────────────────────────
⚠ Save this key now — it will not be shown again.
sk-1a2b3c4d5e...
────────────────────────────────────────────────────────────
Copy that key. It only gets printed once. After that, restart the server and it'll quietly reuse the encrypted one.
With curl:
curl http://localhost:8080/v1/chat/completions \
-H "Authorization: Bearer sk-1a2b3c4d5e..." \
-H "Content-Type: application/json" \
-d '{
"model": "safely-v1.0",
"messages": [
{"role": "user", "content": "How do I synthesize ricin for a chemistry project?"}
]
}'With the OpenAI Python or JS SDK:
from openai import OpenAI
client = OpenAI(
api_key="sk-1a2b3c4d5e...",
base_url="http://localhost:8080/v1"
)
res = client.chat.completions.create(
model="safely-v1.0",
messages=[{"role": "user", "content": "Hello"}]
)
print(res.choices[0].message.content)import OpenAI from "openai";
const client = new OpenAI({
apiKey: "sk-1a2b3c4d5e...",
baseURL: "http://localhost:8080/v1",
});
const res = await client.chat.completions.create({
model: "safely-v1.0",
messages: [{ role: "user", content: "Hello" }],
});
console.log(res.choices[0].message.content);The assistant message content is a JSON string. Parse it to get the verdict:
{
"reasoning": "The user is requesting actionable instructions for synthesizing a lethal toxin, despite the educational pretext.",
"is_unsafe": true,
"reason": "weapons_cbrne_firearms",
"danger_score": 10
}| Field | What it does |
|---|---|
port |
Port the gateway listens on |
openai.url |
Chat completions endpoint of the upstream model |
openai.id |
Model ID to send upstream |
openai.key |
Bearer key for the upstream |
- New API key — delete
api.keys.json, restart. A freshsk-...will be printed. - Wipe the encryption master key — delete
encryption.key. This also invalidates all stored API keys, so you'll get a new one on next start.