Let code inside a VM call OpenAI while your trusted controller holds the real
API key and selects the model. An HTTP egress rule replaces
the authorization header and the request’s top-level model before forwarding
it to OpenAI. The VM uses the official SDK with a placeholder key.
This recipe targets POST /v1/responses. It also shows how to adapt the rule
for Chat Completions, allow the caller to choose a model, and rotate access.
For models accessed through OpenRouter, use the OpenRouter guide; its API path and fallback controls differ from direct OpenAI requests.
Prepare The Controller And Snapshot
Run the controller examples outside the sandbox. Install the SDK with
npm install freestyle@latest, then supply these controller environment variables:
FREESTYLE_API_KEY: your Freestyle API key.FREESTYLE_SNAPSHOT_ID: an Ubuntu-based snapshot containing your application, Node.js,ca-certificates, and theopenaipackage.OPENAI_API_KEY: the provider key to inject.OPENAI_MODEL: the model ID your application should use.
Install the provider SDK in your application’s directory with npm install openai
when preparing the snapshot. Package
installation needs network access during preparation. The application VM below
starts with an empty firewall and receives only the model hostname grant.
Keep both API keys out of application files and snapshots.
Choose an ID from the OpenAI model catalog
that supports the Responses API and your application’s inputs and tools. The
provider key must have access to it. Test application behavior before changing
models; rewriting model does not adapt unsupported parameters or tools.
Inject The Key And Select A Model
Save this as openai-controller.ts in your trusted controller:
import { Freestyle, type CreateTlsRuleOptions } from "freestyle";
function requiredEnv(name: string): string {
const value = process.env[name]?.trim();
if (!value) throw new Error(`Missing ${name}`);
return value;
}
const freestyle = new Freestyle();
const { vmId } = await freestyle.vms.create({
snapshotId: requiredEnv("FREESTYLE_SNAPSHOT_ID"),
firewall: { rules: [] },
});
function openaiRule(apiKey: string, model: string): CreateTlsRuleOptions {
return {
action: "allow",
domain: "api.openai.com",
source: { vmId },
destination: { public: true },
match: { method: ["POST"], path: { exact: "/v1/responses" } },
transform: [
{ headers: { authorization: `Bearer ${apiKey}` } },
{ jsonPatch: [{ op: "add", path: "/model", value: model }] },
],
};
}
const rule = await freestyle.tls.rules.create(
openaiRule(requiredEnv("OPENAI_API_KEY"), requiredEnv("OPENAI_MODEL")),
);
const ruleId = rule.id;
console.log({ vmId, ruleId }); // Retain these IDs in your controller.
OpenAI uses Bearer authentication.
The edge overwrites the guest’s Authorization header, then opens a separate
HTTPS connection that verifies OpenAI’s certificate. Header secrets and patch
values are sealed at rest and read back as "***".
JSON Patch add is an object upsert: /model is inserted if absent and
overwritten if present. It is not a default that the guest can override. See
JSON Patch semantics.
Call OpenAI Inside The VM
Place this file in the application directory that contains the installed SDK:
import OpenAI from "openai";
const openai = new OpenAI({
apiKey: "unused",
baseURL: "https://api.openai.com/v1",
organization: null,
project: null,
});
const response = await openai.responses.create({
model: "selected-by-freestyle",
input: "Reply with one sentence about Linux sandboxes.",
});
console.log(response.model);
console.log(response.output_text);
Run inside the VM, after the TLS rule has configured its hostname and trust store:
NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt node call-openai.mjs
Freestyle installs its CA in the guest’s system trust store. This Ubuntu bundle path makes Node trust the additional CA at process startup. Use your distribution’s bundle path on other images, and restart existing Node processes after trust-store changes. Keep certificate verification enabled.
If the application runs in Docker, the VM’s CA, environment variables, and
hostname mappings are not inherited. Use the Docker TLS recipe
with api.openai.com (or your alias), and pass NODE_EXTRA_CA_CERTS using the
container’s mounted bundle path. For Python clients, see the separate
Requests and HTTPX settings.
The official OpenAI SDK sends JSON
to /v1/responses; it accepts the placeholder strings locally. The edge
replaces both before OpenAI receives the request. The explicit base URL and
null organization/project options avoid inheriting unrelated controller
configuration. If you require OpenAI-Organization or OpenAI-Project, set
those headers in the controller’s header transform too.
For streaming, add stream: true to responses.create and iterate its returned
stream with for await (const event of stream). The request is still JSON;
the SSE response streams through without JSON rewriting.
Choose Which Requests Receive Credentials
The method and path must both match. Query parameters do not affect the path
comparison. An unmatched request is forwarded without either transform;
match does not deny it. For example, GET /v1/models keeps the guest’s
placeholder credential and receives no injected key.
Keep the VM’s firewall restricted and review other TLS grants. Broad public egress can let a workload bypass the transformed route using credentials it obtains elsewhere. All processes in the selected VM can use this grant; it does not distinguish binaries.
For Chat Completions,
change the rule’s exact path to /v1/chat/completions and use
openai.chat.completions.create({ model, messages }). The same /model patch
works. This replaces the Responses grant; it does not enable both endpoints.
To expose both with separate transforms, use distinct domain aliases and SDK
base URLs, as shown in the OpenShell migration guide.
Creating two rules with the same domain/source and different match values
does not create a path router: matching happens after TLS rule selection.
To let the guest choose a model while keeping the key outside the VM, remove
the jsonPatch entry from openaiRule. Retain match and the header transform,
and supply a real model ID in the guest’s SDK request. Removing match too
grants the injected credential to every HTTP request on the selected rule,
including endpoints outside this recipe.
Change The Model, Rotate The Key, Or Remove Access
Using the same controller function and saved ruleId, replace the full rule:
await freestyle.tls.rules.update(
ruleId,
openaiRule(requiredEnv("OPENAI_API_KEY"), requiredEnv("OPENAI_MODEL")),
);
Load the new key or model into the controller before updating. Always include the actual secret and model value; a redacted API read cannot reconstruct them. Cached decisions can take about five seconds to refresh. Existing requests continue with their original configuration. Verify a new request succeeds before revoking the old provider key.
Delete the grant when it is no longer needed:
await freestyle.tls.rules.delete(ruleId);
Deletion removes this grant after caches refresh; it does not revoke the key at OpenAI. Other firewall and TLS rules still apply. Delete the VM separately when the task is complete.
Verify The Integration And Handle Other API Shapes
Make a small real request and inspect response.model and provider usage. An
alias may resolve to a versioned model ID. Change the controller’s model, wait
for refresh, and verify a subsequent request. Provider requests use your
account’s normal billing.
For a 401, check the method/path, injected key, and any required project
headers. A certificate error calls for checking the guest CA bundle. A 400
or model-access error can indicate parameters incompatible with the selected
model. See transform limits and errors
for edge-generated failures.
This rule does not authenticate response retrieval, model discovery, file
uploads, or batch requests. Multipart uploads, JSONL batches, and Realtime
WebSocket sessions need their own handling. A top-level /model patch cannot
control models inside uploaded batch files or impose a spending limit. Use
provider permissions and an application gateway when you need those controls.