Use OpenRouter to access models from multiple providers through one API. Freestyle supplies the VM and injects your OpenRouter key at the edge; OpenRouter handles the upstream model request. Your guest can use the OpenAI SDK with a placeholder key.
This recipe targets POST /api/v1/chat/completions. It sets the requested
model and fallback list in your trusted controller and shows how to select an
upstream provider separately.
Prepare The Controller And Snapshot
Install freestyle@latest in your controller, outside the VM. Supply these
environment variables there:
FREESTYLE_API_KEY: your Freestyle API key.FREESTYLE_SNAPSHOT_ID: an Ubuntu-based snapshot containing your application, Node.js,ca-certificates, and theopenaipackage.OPENROUTER_API_KEY: your OpenRouter API key.OPENROUTER_MODEL: a model ID from the OpenRouter catalog, including its namespace.
Install the guest SDK with npm install openai in your application’s directory
before taking the snapshot. Install
dependencies while the builder has network access. Keep both API keys outside
the snapshot; the application VM below starts with an empty firewall.
Choose a concrete catalog model if you want a fixed selection. Router IDs and latest aliases intentionally let OpenRouter choose or change the underlying model. Check that your selected model supports your application’s inputs and tools. This guide uses an OpenRouter key, not a direct OpenAI or Anthropic key.
Inject Authentication And Set The Model List
Run this in the 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 openrouterRule(apiKey: string, model: string): CreateTlsRuleOptions {
return {
action: "allow",
domain: "openrouter.ai",
source: { vmId },
destination: { public: true },
match: { method: ["POST"], path: { exact: "/api/v1/chat/completions" } },
transform: [
{ headers: { authorization: `Bearer ${apiKey}` } },
{ jsonPatch: [
{ op: "add", path: "/model", value: model },
{ op: "add", path: "/models", value: [model] },
] },
],
};
}
const rule = await freestyle.tls.rules.create(
openrouterRule(requiredEnv("OPENROUTER_API_KEY"), requiredEnv("OPENROUTER_MODEL")),
);
const ruleId = rule.id;
console.log({ vmId, ruleId }); // Retain these IDs in your controller.
OpenRouter uses Bearer authentication.
The edge overwrites the guest’s Authorization header and opens a separate
HTTPS connection that verifies OpenRouter’s certificate. Header secrets and
patch values are sealed at rest and read back as "***".
OpenRouter also accepts a models fallback array.
Changing only /model would leave a caller’s fallback list intact. This recipe
replaces both fields with the controller’s selection. JSON Patch add upserts
object members: it creates either field when missing and overwrites it when
present. Setting /models replaces the whole array; it does not append.
Optional app attribution headers,
such as HTTP-Referer and X-OpenRouter-Title, can go in the same header
transform when your controller should set them. They are not authentication.
Call OpenRouter Inside The VM
Place this file in the directory with the installed openai package:
import OpenAI from "openai";
const openrouter = new OpenAI({
apiKey: "unused",
baseURL: "https://openrouter.ai/api/v1",
organization: null,
project: null,
});
const completion = await openrouter.chat.completions.create({
model: "selected-by-freestyle",
messages: [{ role: "user", content: "Reply with one sentence about Linux sandboxes." }],
});
console.log(completion.model);
console.log(completion.choices[0]?.message.content);
Run inside the VM, after the TLS rule has configured the hostname and trust store:
NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt node call-openrouter.mjs
Freestyle installs its CA in the guest’s system trust store. On Ubuntu, this bundle makes Node trust that CA at startup. Use the correct bundle path for other distributions and restart existing Node processes after trust-store changes. Keep TLS verification enabled.
For Docker, use the container TLS recipe
with openrouter.ai (or your alias), retaining /api/v1 in the SDK base URL.
The VM’s CA, environment variables, and hostname mappings are not inherited;
set NODE_EXTRA_CA_CERTS to the mounted bundle inside the container. Python
clients have separate Requests and HTTPX settings.
OpenRouter’s OpenAI SDK integration
uses the base URL https://openrouter.ai/api/v1. The SDK appends
/chat/completions, producing the exact path matched by this rule. The request
is JSON. Adding stream: true and iterating the returned stream enables SSE;
Freestyle streams the response through without rewriting it.
Separate Model Fallbacks From Provider Failover
Model fallbacks choose another model. To allow a controller-selected
fallback, change the /models value in the existing patch to an ordered list
containing the primary model and approved fallback IDs. Keep /model set to
the primary. OpenRouter performs the retries; Freestyle only writes the list.
To let the guest choose both, remove the whole jsonPatch entry and retain
the matched header transform.
Provider failover chooses another host serving a model. Fixing model
and models does not pin its serving provider. OpenRouter’s
provider preferences
control that independently. To select a specific provider, append this operation
to the existing jsonPatch array, replacing provider-slug with a slug that
serves your chosen model:
{
"op": "add",
"path": "/provider",
"value": {
"order": ["provider-slug"],
"allow_fallbacks": false
}
}
This replaces the entire provider object, including any guest preferences.
Include any other required provider settings in that object. With provider
fallbacks disabled, an unavailable selected provider can make the request fail.
These patches configure specific request fields. They do not remove other features such as Auto Router plugins or presets. Assign OpenRouter guardrails to the injected API key for enforced model/provider allowlists and spending limits. Audit those features before treating a patched model field as a complete access policy.
Preserve An Alias Or Use Another Endpoint
To retain inference.local, change the controller rule’s domain to
inference.local and its destination to { host: "openrouter.ai", port: 443 }.
Set the guest SDK’s base URL to https://inference.local/api/v1. Keep the
exact match /api/v1/chat/completions; the destination host does not add /api
or rewrite the path. See the OpenShell migration guide.
The rule injects credentials only on the matched POST path. An unmatched
request is forwarded with its original headers and body; match does not
deny it. Model discovery and other endpoints receive no real key from this
rule. Keep the VM firewall restricted and review other
TLS grants; all processes in the selected VM can use this grant.
OpenRouter also has Responses and Anthropic-compatible endpoints, but this recipe grants Chat Completions only. Verify each endpoint’s request schema and fallback fields before adding it. Use distinct aliases for separately matched routes: multiple rules with the same domain/source do not dispatch by HTTP path.
Rotate Access And Verify Requests
After changing the controller’s key or model, replace the full rule using the
same function and saved ruleId:
await freestyle.tls.rules.update(
ruleId,
openrouterRule(requiredEnv("OPENROUTER_API_KEY"), requiredEnv("OPENROUTER_MODEL")),
);
Keep any additional provider operation in that function too. Include actual secret values on every update; redacted reads cannot reconstruct them. Cached decisions can take about five seconds to refresh. In-flight requests continue with their original configuration.
Make a small real request and check completion.model and OpenRouter usage.
Requests are billed through OpenRouter. Verify a fresh request with the new
key before revoking the old one. For 401, check the key and the full matched
path, including /api. Model or provider errors can indicate unavailable
models, incompatible parameters, or restrictive routing preferences.
Delete the grant when it is no longer needed:
await freestyle.tls.rules.delete(ruleId);
Deletion removes this grant after cache refresh; it does not revoke the key at OpenRouter. Delete the VM separately when done. See JSON transform limits for body limits and edge errors. Patched requests must be uncompressed JSON; multipart uploads and WebSocket sessions require separate handling.