🔥

HonoとCloudflare WorkersでAzure OpenAI Serviceのproxyを作った話

2023/05/22に公開

こんにちは、みてねコールドクターでエンジニアマネージャーをしている遠藤です。
今日は、HonoとCloudflare Workersを使用して、Azure OpenAI Serviceのproxyを作った話をしたいと思います。

背景

先日この記事で紹介しましたが、弊社ではChatGPTの活用を始めており、Azure OpenAI Serviceを経由して利用しています。ただ、Azureでは1つのエンドポイントに対して2つのAPIキーしか用意されていないため、複数のサービスで使いたい場合には必要なキーの分だけエンドポイントを作成する必要があります。そのため、管理コストが増えることになります。例えば、先日CodeZineで紹介されていた「Genie AI」を利用する場合、メンバー分エンドポイントを作るか、キーを共有する必要がありました。

そこで、HonoとCloudflare Workersを使用して、Azure OpenAI Serviceのproxyを作成し、認証はHonoで行うようにしました。

なぜHonoを選んだのか

Honoは、エッジサーバーで動作するウェブフレームワークです。small, simple, ultrafastがコンセプトなので、今回のようなProxyを作る際にパフォーマンスを低下させずに、機能を追加しやすくなるということから、Honoを選択しました。

実装

このproxyの実装は以下のように行いました。

index.js
import { Hono } from 'hono';
import { logger } from 'hono/logger'
import { stream } from './utils/stream'
import { auth } from './utils/auth'

const apiVersion = "2023-03-15-preview";
const app = new Hono();

app.use("*", logger());

app.post('/v1/chat/completions', auth(), async (c) => {
  const fetchAPI = `https://${c.env.RESOURCE_NAME}.openai.azure.com/openai/deployments/${c.env.DEPLOY_NAME_GPT35}/chat/completions?api-version=${apiVersion}`;

  const body = await c.req.json();

  const payload = {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "api-key": c.env.API,
    },
    body: JSON.stringify(body),
  };

  const response = await fetch(fetchAPI, payload);

  if (body?.stream != true) {
    return response;
  }

  let { readable, writable } = new TransformStream();
  stream(response.body, writable);

  return new Response(readable, response);
});


app.options("*", (c) => {
  return new Response(null, {
    headers: {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Methods': '*',
      'Access-Control-Allow-Headers': '*'
    }
  })
})

export default app;

utils/stream.js
export async function stream(readable, writable) {
  const reader = readable.getReader();
  const writer = writable.getWriter();
  const encoder = new TextEncoder();
  const decoder = new TextDecoder();
  const newline = "\n";
  const delimiter = "\n\n";
  const encodedNewline = encoder.encode(newline);

  let buffer = "";
  while (true) {
    let { value, done } = await reader.read();
    if (done) {
      break;
    }
    buffer += decoder.decode(value, { stream: true });
    let lines = buffer.split(delimiter);

    for (let i = 0; i < lines.length - 1; i++) {
      await writer.write(encoder.encode(lines[i] + delimiter));
      await sleep(30);
    }

    buffer = lines[lines.length - 1];
  }

  if (buffer) {
    await writer.write(encoder.encode(buffer));
  }
  await writer.write(encodedNewline);
  await writer.close();
}

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}
utils/auth.js
const TOKEN_STRINGS = '[A-Za-z0-9._~+/-]+=*'
const PREFIX = 'Bearer'

export function auth() {
  return async (c, next) => {
    const headerToken = c.req.headers.get("authorization");
    const regexp = new RegExp('^' + PREFIX + ' +(' + TOKEN_STRINGS + ') *$')
    const match = regexp.exec(headerToken)

    if (!match) {
      // Invalid Request
      c.res = new Response('{"message":"authentication error"}', {
        status: 401,
      });
      return ;
    } else {
      const equal = await c.env.API_KEY.get(match[1])

      if (!equal) {
        c.res = new Response('{"message":"authentication error"}', {
          status: 401,
        });
        return;
      }
    }

    await next();
  };
}

全体の流れは以下の通りです。

  1. Honoでリクエストを受け取り、authで認証を行う
    • APIキーの保存にはCloudflare KVを使用しています。
  2. 受け取ったJSONをそのままAzure OpenAI Serviceにリクエストを送る
    • このとき、環境変数に保存されたAzure OpenAI ServiceのAPIキーを使用します。
  3. Azure OpenAI Serviceからのレスポンスをストリーミングで返します。

現状、KVには生のAPIキーが保存されているため、可能であればCloudflare Access等で認証するとより安全です。ただし、今回は諸事情によりこのような実装となりました。

まとめ

Honoを使用して簡単にプロキシを構築することができました。ログの保存や認証の追加といった作業もHonoを使えば簡単に実行できますので、今後も積極的に活用したいと思います。

Cloudflareの積極的な利用をしたいエンジニアを募集しています。

参考

https://zenn.dev/razokulover/articles/ac84a141abee86
https://github.com/haibbo/cf-openai-azure-proxy

みてねコールドクターテックブログ

Discussion