Freestyle Docs

Freestyle / Guides

Select OpenRouter Models and Inject Authentication

Call OpenRouter from a sandbox with edge-injected credentials, choose models and fallback lists in your controller, and configure provider routing.

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 the openai package.
  • 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.

esc