Прокси на Scala: безопасный и масштабируемый доступ к OpenAI API
При работе с OpenAI API в корпоративных и продуктовых проектах часто возникают задачи:
-
централизовать доступ к API
-
логировать запросы и ответы
-
ограничить доступ по IP или токену
-
обеспечить доступ из ограниченных регионов
Эти задачи удобно решаются через промежуточный прокси-сервис.
В этой статье разбирается реальное приложение на Scala 3, которое:
-
прозрачно проксирует HTTP-запросы
-
не меняет структуру API
-
легко разворачивается на VPS
-
готово к расширению
Архитектура решения
Поток запроса:
Клиент → HTTPS (Apache/Nginx) → Scala proxy → OpenAI API
Принцип работы:
-
клиент отправляет запрос на ваш домен
-
reverse-proxy принимает HTTPS
-
Scala-сервис проксирует запрос в OpenAI
-
ответ возвращается клиенту
Настройка проекта (Scala + http4s)
build.sbt
val scala3Version = "3.3.6"
lazy val root = project
.in(file("."))
.settings(
name := "openai-proxy",
version := "0.1.0",
scalaVersion := scala3Version,
libraryDependencies ++= Seq(
"org.http4s" %% "http4s-dsl" % "0.23.30",
"org.http4s" %% "http4s-ember-server" % "0.23.30",
"org.http4s" %% "http4s-ember-client" % "0.23.30",
"org.typelevel" %% "cats-effect" % "3.5.4",
"org.typelevel" %% "log4cats-slf4j" % "2.7.0",
"ch.qos.logback" % "logback-classic" % "1.5.16",
"com.typesafe" % "config" % "1.4.3"
)
)
Конфигурация приложения
application.conf
server {
host = "127.0.0.1"
port = 8080
}
openai {
base-url = "https://api.openai.com"
}
AppConfig.scala
final case class AppConfig(
host: String,
port: Int,
baseUrl: String
)
object AppConfig {
def load(): AppConfig = {
val cfg = com.typesafe.config.ConfigFactory.load()
AppConfig(
host = cfg.getString("server.host"),
port = cfg.getInt("server.port"),
baseUrl = cfg.getString("openai.base-url").stripSuffix("/")
)
}
}
Реализация прокси
Основная логика реализована в ProxyRoutes
Удаление hop-by-hop заголовков
val HopHeaders = Set(
"Connection",
"Keep-Alive",
"Transfer-Encoding",
"Upgrade"
)
def cleanHeaders(headers: Headers): Headers =
Headers(headers.headers.filterNot(h => HopHeaders.contains(h.name.toString)))
Проксирование запроса
def proxy(client: Client[IO], baseUrl: String): HttpApp[IO] =
HttpApp[IO] { req =>
val targetUri = Uri
.unsafeFromString(baseUrl)
.withPath(req.uri.path)
.copy(query = req.uri.query)
val proxiedRequest = Request[IO](
method = req.method,
uri = targetUri,
headers = cleanHeaders(req.headers),
body = req.body
)
client.run(proxiedRequest).use { resp =>
Response[IO](
status = resp.status,
headers = resp.headers,
body = resp.body
).pure[IO]
}
}
Логирование
def logRequest(req: Request[IO]): IO[Unit] =
IO.println(s">>> ${req.method} ${req.uri.path}")
def logResponse(status: Status): IO[Unit] =
IO.println(s"<<< $status")
Использование:
for {
_ <- logRequest(req)
response <- client.run(proxiedRequest).use { resp =>
logResponse(resp.status) *>
IO.pure(Response[IO](resp.status).withEntity(resp.body))
}
} yield response
Обработка ошибок
.handleErrorWith { e =>
IO.println(s"Error: ${e.getMessage}") *>
IO.pure(Response[IO](Status.BadGateway).withEntity("Proxy error"))
}
Точка входа приложения
object Main extends IOApp {
override def run(args: List[String]): IO[ExitCode] = {
val config = AppConfig.load()
EmberClientBuilder.default[IO].build.use { client =>
val app = proxy(client, config.baseUrl)
EmberServerBuilder.default[IO]
.withHost(ipv4"127.0.0.1")
.withPort(port"${config.port}")
.withHttpApp(app)
.build
.use(_ => IO.never)
}.as(ExitCode.Success)
}
}
Примеры запросов
Получение моделей:
curl http://localhost:8080/v1/models \
-H "Authorization: Bearer sk-..."
Chat completion:
curl http://localhost:8080/v1/chat/completions \
-H "Authorization: Bearer sk-..." \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-4o",
"messages": [
{"role": "user", "content": "Привет"}
]
}'
Streaming:
curl -N http://localhost:8080/v1/chat/completions \
-H "Authorization: Bearer sk-..." \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-4o",
"messages": [{"role": "user", "content": "Расскажи шутку"}],
"stream": true
}'
Деплой на сервер
systemd сервис:
[Service]
ExecStart=/usr/bin/java -jar app.jar
Restart=always
Apache reverse proxy:
ProxyPass / http://127.0.0.1:8080/
ProxyPassReverse / http://127.0.0.1:8080/
Практические доработки
Добавление токена доступа:
val ProxyToken = "secret123"
def auth(req: Request[IO]): Boolean =
req.headers.get(CIString("X-Proxy-Token"))
.exists(_.head.value == ProxyToken)
Простое ограничение запросов:
var counter = 0
def limit(): IO[Unit] =
IO {
counter += 1
if (counter > 1000) throw new Exception("Rate limit exceeded")
}
Health-check endpoint:
case GET -> Root / "health" =>
Ok("ok")
Когда это применимо
-
SaaS с интеграцией LLM
-
Telegram-боты
-
AI-агенты
-
внутренние корпоративные сервисы
Заключение
Прокси на Scala — это компактное и управляемое решение для работы с OpenAI API.
Он позволяет централизовать доступ, контролировать использование и постепенно расширять функциональность без изменений клиентских приложений.
По мере развития проекта в него можно добавлять авторизацию, биллинг, метрики и поддержку нескольких AI-провайдеров.