File size: 3,472 Bytes
e1d8498
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f6dd1f9
 
 
 
 
 
e1d8498
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f6dd1f9
 
e1d8498
 
 
 
 
 
 
 
 
 
 
 
 
f6dd1f9
e1d8498
 
 
f6dd1f9
e1d8498
 
 
 
 
f6dd1f9
e1d8498
 
 
 
f6dd1f9
e1d8498
f6dd1f9
e1d8498
 
 
 
 
 
 
 
 
 
 
f6dd1f9
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
"use strict";

const express = require("express");
const cors = require("cors");
const helmet = require("helmet");
const morgan = require("morgan");
const rateLimit = require("express-rate-limit");

const { logger } = require("./logger");
const modelsRouter = require("./routes/models");
const statsRouter = require("./routes/stats");
const db = require("./database");
const litellm = require("./litellm");

const app = express();
const PORT = parseInt(process.env.PORT || "3001", 10);

// BUG FIX: Set trust proxy BEFORE rate limiter.
// nginx runs in front of backend and sets X-Forwarded-For header.
// Without trust proxy, express-rate-limit throws ERR_ERL_UNEXPECTED_X_FORWARDED_FOR
// and logs a ValidationError on every single request.
// Value 1 = trust first proxy hop (nginx on localhost).
app.set("trust proxy", 1);

app.use(helmet({ crossOriginResourcePolicy: false }));
app.use(
  cors({
    origin: "*",
    methods: ["GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS"],
    allowedHeaders: ["Content-Type", "Authorization"],
  })
);
app.use(express.json({ limit: "10mb" }));
app.use(
  morgan("combined", {
    stream: { write: (msg) => logger.http(msg.trim()) },
    skip: (req) => req.url === "/api/health",
  })
);

app.use(
  "/api/",
  rateLimit({
    windowMs: 60 * 1000,
    max: 200,
    standardHeaders: true,
    legacyHeaders: false,
    message: { success: false, error: "Too many requests" },
  })
);

app.use("/api/models", modelsRouter);
app.use("/api", statsRouter);

app.use((req, res) => {
  res.status(404).json({ success: false, error: "Not found" });
});

app.use((err, req, res, _next) => {
  logger.error("Unhandled error", { error: err.message, stack: err.stack });
  res.status(500).json({ success: false, error: "Internal server error" });
});

async function start() {
  db.getDb();
  logger.info("Database initialized");
  syncModelsToLitellmWithRetry();
  app.listen(PORT, "0.0.0.0", () => {
    logger.info("AI Gateway Backend running on port " + PORT);
    logger.info("Gateway public URL: " + (process.env.GATEWAY_PUBLIC_URL || "http://localhost"));
  });
}

async function syncModelsToLitellmWithRetry() {
  const MAX_ATTEMPTS = 10;
  const BASE_DELAY_MS = 5000;
  for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
    try {
      await litellm.healthCheck();
      await syncModelsToLitellm();
      return;
    } catch (err) {
      const delay = Math.min(BASE_DELAY_MS * attempt, 30000);
      logger.warn("LiteLLM not ready (attempt " + attempt + "/" + MAX_ATTEMPTS + "), retrying in " + delay + "ms...");
      await new Promise((r) => setTimeout(r, delay));
    }
  }
  logger.error("LiteLLM did not become ready after all retry attempts.");
}

async function syncModelsToLitellm() {
  try {
    const models = db.listModels({ enabledOnly: true });
    logger.info("Syncing " + models.length + " models to LiteLLM...");
    for (const model of models) {
      try {
        const litellmId = await litellm.registerModel({ ...model, _apiKey: model._apiKey });
        db.updateModel(model.id, { litellmId });
        logger.info("Synced: " + model.name);
      } catch (err) {
        logger.warn("Failed to sync model " + model.name + ": " + err.message);
      }
    }
    logger.info("Model sync complete");
  } catch (err) {
    logger.error("Model sync failed", { error: err.message });
  }
}

start().catch((err) => {
  logger.error("Fatal startup error", { error: err.message });
  process.exit(1);
});