HonoとCloudflare WorkersでAzure OpenAI Serviceのproxyを作った話
こんにちは、みてねコールドクターでエンジニアマネージャーをしている遠藤です。
今日は、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の実装は以下のように行いました。
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;
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));
}
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();
};
}
全体の流れは以下の通りです。
- Honoでリクエストを受け取り、authで認証を行う
- APIキーの保存にはCloudflare KVを使用しています。
- 受け取ったJSONをそのままAzure OpenAI Serviceにリクエストを送る
- このとき、環境変数に保存されたAzure OpenAI ServiceのAPIキーを使用します。
- 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