Create An OpenAI Proxy Server using Express and Https

·

8 min read

Introduction

OpenAI API does not support some countries. That is the barrier for some third-country developers to access and explore this technology.

In this article, we will make OpenAI API more accessible using a proxy to transport back and forth the requests and responses between OpenAI and end users

As you may know, the proxy server is an intermediate server that forwards the request and response between the client and the target server. So let's create that

Setup the project

Let's create a new folder name openai-proxy and install express (to receive requests and responses from clients) and https (to send requests and get responses from OpenAI). We will also create a new index.js file to handle the proxy logic

mkdir openai-proxy
cd openai-proxy
npm install express https
touch index.js

Host client requests and responses

In index.js file, let's create a basic express server to receive the client request and response with basic hello world sentence:

const express = require('express');
const app = express(); // create an instance of express
app.use(express.json()); // for parsing application/json

// Setup the server to listen on port 3000 and print the log in the console
app.listen(3000, () => {
  console.log('listening on port 3000');
});

// Route to return hello world
app.get('/', (req, res) => {
  res.send('hello world');
})

Run the server, we should see the console log

node index.js
# listening on port 3000

Caution*: Every time we change the code, we need to restart the server to apply the new code!*

When testing with the first request, we should see the hello world sentence

curl "localhost:3000"
# hello world%

Make requests and get responses from OpenAI

Now it's time to call OpenAI API. To send a request to OpenAI we will need the OpenAI API key which can be created from

To ask OpenAI to generate the "Hello, World!" response, we will use the payload:

{
    "model": "gpt-3.5-turbo",
    "messages": [
        {
            "role": "user",
            "content": "Say exactly this: Hello, World!"
        }
    ],
    "temperature": 0.7
}

We will create the POST request to the https://api.openai.com/v1/chat/completions domain. I created it in the curl so you can test it in your terminal, be noted to replace the OPENAI_API_KEY with your real key

curl -X POST -H "Content-Type: application/json" -H "Authorization: Bearer OPENAI_API_KEY" -d '{
    "model": "gpt-3.5-turbo",
    "messages": [
        {
            "role": "user",
            "content": "Say exactly this: Hello, World!"
        }
    ],
    "temperature": 0.7
}' "https://api.openai.com/v1/chat/completions"

Run this curl in bash terminal and you will get this result:

{
  "id": "chatcmpl-829hRBwMu5JT2cu00kKOLrPIMshQr",
  "object": "chat.completion",
  "created": 1695524633,
  "model": "gpt-3.5-turbo-0613",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "Hello, World!"
      },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 15,
    "completion_tokens": 4,
    "total_tokens": 19
  }
}

The value in the path Response.choices[0].message.content is Hello, World! which is what we expected from OpenAI API

Now instead of using curl, we transfer this to our code in index.js. Add the following code to our index.js

// ...
const https = require('https'); // Add this to the top of index.js
// ...
app.get("/", (req, res) => {
  res.send("hello world");
  callOpenAI(); // Add this
});
// Add this new function
const callOpenAI = async () => {
  const options = {
    hostname: "api.openai.com",
    path: "/v1/chat/completions",
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
        // We not put OPENAI_API_KEY in the code because it is secret.
        // We will add it in the terminal later
    },
  };

  // Payload is what we will send to OpenAI API
  const payload = {
    model: "gpt-3.5-turbo",
    messages: [
      {
        role: "user",
        content: "Say exactly this: Hello, World!",
      },
    ],
    temperature: 0.7,
  };

  // Create a request object with the options
  // It will be called by the request.end() function
  const openaiRequest = https.request(options, (openaiResponse) => {
      let data = "";

      // We receive the response in chunks via the "data" event
      // Don't worry much. If you don't understand,
      // just treat it as the boilerplate of https package
      // to send request and receive response.
      openaiResponse.on("data", (chunk) => {
        data += chunk;
      });

      // We are done receiving the response when the "end" event is emitted
      openaiResponse.on("end", () => {
        console.log("Full data response: ", JSON.parse(data));
        console.log("Content response: ", JSON.parse(data).choices[0].message.content);
      });
    });

  openaiRequest.write(JSON.stringify(payload)); // Add the payload to the request

  openaiRequest.end(); // Activate the request
};

Let's restart our server, this time we also provide the OPENAI_API_KEY environment variable. Be noted to replace the sk-X with your OpenAI API key:

OPENAI_API_KEY=sk-XXXXXXXXXXXXXX node index.js
# listening on port 3000

Making a request to our server and we will see the result in our terminal as below:

curl localhost:3000
# {
#   id: 'chatcmpl-829uYKIQ5TRtqUwGdtqTjkK24Xg0p',
#   object: 'chat.completion',
#   created: 1695525446,
#   model: 'gpt-3.5-turbo-0613',
#   choices: [ { index: 0, message: [Object], finish_reason: 'stop' } ],
#   usage: { prompt_tokens: 15, completion_tokens: 4, total_tokens: 19 }
# }
# Content response:  Hello, World!

Now let's do a final step: Instead of hardcoding the options and payload, we will get it from the client request, and then return the OpenAI response to the client response. This time we act exactly like a proxy

Proxy client request and response to OpenAI server

First, refactor the callOpenAI function so that its parameters will be also the req and res variables from the express router. The idea is like this:

// Change from this
app.get("/", (req, res) => {});
const callOpenAI = () => {};

// Change to this, so we can pass the `req` and `res` variable to the
// callOpenAI function. Later we can read and write data from them.
// Also note that we use app.all("*") to accept both GET and POST request and accept all endpoints
app.all("*", (req, res) => {
  callOpenAI(req, res);
});
const callOpenAI = async (clientRequest, clientResponse) => {};

We will change options data (responsible for domain, request headers) from hard coding to use the client request (req):

// Change from this
const options = {
    hostname: "api.openai.com",
    path: "/v1/chat/completions",
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
    },
  };

// Change to this
const options = {
    hostname: "api.openai.com",
    path: clientRequest.path, // use client request path
    method: clientRequest.method, // use client request method
    headers: {
      "Content-Type": clientRequest.headers['content-type'], // use client request headers
      Authorization: `Bearer ${process.env.OPENAI_API_KEY}`, // Only OPEN AI API key is provided by the proxy
    },
  };

We also remove the hardcoded payload and use payload from client request instead

// Remove this
const payload = {
    model: "gpt-3.5-turbo",
    messages: [
      {
        role: "user",
        content: "Say exactly this: Hello, World!",
      },
    ],
    temperature: 0.7,
  };

// Change from this
openaiRequest.write(JSON.stringify(payload));

// Change to this
openaiRequest.write(JSON.stringify(clientRequest.body)); // The hardcode payload is removed

Finally, we change the openaiRequest object to this:

// Change from this  
const openaiRequest = https.request(options, (openaiResponse) => {

    let data = "";

    // We receive the response in chunks via the "data" event
    openaiResponse.on("data", (chunk) => {
      data += chunk;
    });

    // We are done receiving the response when the "end" event is emitted
    openaiResponse.on("end", () => {
      console.log("Full data response: ", JSON.parse(data));
      console.log("Content response: ", JSON.parse(data).choices[0].message.content);
    });
  });

// Change to this. We do nothing than immediately transport the data packages from OpenAI to our client
const openaiRequest = https.request(options, (openaiResponse) => {

    // Forward the header from OpenAI to clientResponse for correct response format
    clientResponse.setHeader("Content-Type", openaiResponse.headers["content-type"]);

    // We receive the response in chunks via the "data" event
    openaiResponse.on("data", (chunk) => {
      clientResponse.write(chunk);
    });

    // We are done receiving the response when the "end" event is emitted
    openaiResponse.on("end", () => {
      clientResponse.end();
    });
  });

Here is the full code

const express = require("express");
const https = require("https");

const app = express(); // create an instance of express
app.use(express.json()); // for parsing application/json

// Setup the server to listen on port 3000 and print the log in the console
app.listen(3000, () => {
  console.log("listening on port 3000");
});

// Route to return hello world
app.all("*", (req, res) => {
  callOpenAI(req, res);
});

const callOpenAI = async (clientRequest, clientResponse) => {
  const options = {
    hostname: "api.openai.com",
    path: clientRequest.path, // use client request path
    method: clientRequest.method, // use client request method
    headers: {
      "Content-Type": clientRequest.headers['content-type'] || "application/json", // use client request headers
      Authorization: `Bearer ${process.env.OPENAI_API_KEY}`, // Only OPEN AI API key is provided by the proxy
    },
  };

  // Create a request object with the options
  // It will be called by the request.end() function
  const openaiRequest = https.request(options, (openaiResponse) => {

    clientResponse.setHeader("Content-Type", openaiResponse.headers["content-type"]);

    // We receive the response in chunks via the "data" event
    openaiResponse.on("data", (chunk) => {
      clientResponse.write(chunk);
    });

    // We are done receiving the response when the "end" event is emitted
    openaiResponse.on("end", () => {
      clientResponse.end();
    });
  });

  openaiRequest.write(JSON.stringify(clientRequest.body)); // Add the payload to the request

  openaiRequest.end(); // Activate the request
};

Let's restart our server and test it with curl. This time instead of using OpenAI domain, we use our proxy domain. Be noted to replace the OPENAI_API_KEY with your OpenAI API key:

curl -X POST -H "Content-Type: application/json" -H "Authorization: Bearer OPENAI_API_KEY" -d '{
    "model": "gpt-3.5-turbo",
    "messages": [
        {
            "role": "user",
            "content": "Say exactly this: Hello, World!"
        }
    ],
    "temperature": 0.7
}' "http://localhost:3000/v1/chat/completions"

You will get the response

{
  "id": "chatcmpl-82BAhc8dzYwA4GtwmNgm0DJVqwNpa",
  "object": "chat.completion",
  "created": 1695530291,
  "model": "gpt-3.5-turbo-0613",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "Hello, World!"
      },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 15,
    "completion_tokens": 4,
    "total_tokens": 19
  }
}

Recap

We finished proxy the client request to OpenAI API. Now the client and use OpenAI API without needing the key.

Full sourcecode can be found here: https://github.com/votanlean/openai-proxy

We can develop further features:

  1. Create the api key system to manage the client access to our proxy

  2. Implement tiktoken library to calculate the token usage from client request prompt and from OpenAI response

Thank you for reading