github-actions[bot] commited on
Commit
a3cd373
·
1 Parent(s): d0b546b

deploy: d10e28b — 更新 litellm.js

Browse files
Files changed (1) hide show
  1. backend/src/litellm.js +113 -211
backend/src/litellm.js CHANGED
@@ -1,250 +1,152 @@
1
  "use strict";
2
 
3
  /**
 
 
 
4
 
5
- - LiteLLM Proxy Management Client
6
- - Wraps LiteLLM’s admin API for dynamic model registration.
7
- */
8
 
9
- const axios = require(“axios”);
10
- const { logger } = require(”./logger”);
11
-
12
- const LITELLM_BASE_URL = process.env.LITELLM_BASE_URL || “http://litellm:4000”;
13
- const LITELLM_MASTER_KEY = process.env.LITELLM_MASTER_KEY || “sk-gateway-master-key”;
14
 
15
  const client = axios.create({
16
- baseURL: LITELLM_BASE_URL,
17
- timeout: 15000,
18
- headers: {
19
- Authorization: `Bearer ${LITELLM_MASTER_KEY}`,
20
- Content-Type: application/json,
21
- },
22
  });
23
 
24
- // ─── Model Registration ────────────────────────────────────────────────────
25
-
26
- /**
27
-
28
- - Register a new model in LiteLLM at runtime.
29
- - @param {object} model - Our internal model record
30
- - @returns {string} - LiteLLM internal model ID
31
- */
32
- async function registerModel(model) {
33
  const payload = {
34
- model_name: model.name, // OpenAI-compatible alias exposed to callers
35
- litellm_params: buildLitellmParams(model),
36
- model_info: {
37
- id: model.id,
38
- description: model.description || “”,
39
- model_type: model.modelType || chat,
40
- },
41
  };
42
 
43
- logger.info(Registering model with LiteLLM, { modelName: model.name });
44
-
45
- const response = await client.post(/model/new, payload);
46
-
47
- // ─── BUG FIX #7: Ghost model on missing LiteLLM ID ──────────────────────
48
- //
49
- // ORIGINAL CODE:
50
- // const litellmId = response.data?.model_info?.id || response.data?.id || model.id;
51
- //
52
- // The final `|| model.id` fallback silently stored our own UUID as
53
- // `litellm_id` whenever LiteLLM returned a response without a recognisable
54
- // ID field. This created a “ghost model” scenario:
55
- //
56
- // 1. DB stored our own UUID as `litellm_id`.
57
- // 2. On delete, deregisterModel() sent that UUID to LiteLLM /model/delete.
58
- // 3. LiteLLM couldn’t find it → returned an error we silently swallowed.
59
- // 4. Model removed from our DB but remained live inside LiteLLM’s in-memory
60
- // router still accepting real API traffic indefinitely.
61
- //
62
- // FIX: Extract the ID from the two documented response paths. If neither
63
- // yields an ID, emit a structured WARN (visible in logs/alerts) then fall
64
- // back to model.id only as a last resort so the creation flow is not broken.
65
- // The warning makes the deregistration risk explicit to operators.
66
- //
67
- // LiteLLM /model/new documented response shapes:
68
- // v1.x+ → { model_info: { id: “<uuid>”, … }, model_name: “…” }
69
- // older → { id: “<uuid>”, … }
70
- // ─────────────────────────────────────────────────────────────────────────
71
- const litellmId =
72
- response.data?.model_info?.id ||
73
- response.data?.id ||
74
- null;
75
-
76
- if (!litellmId) {
77
- logger.warn(
78
- “[BUG#7] LiteLLM /model/new response contained no recognisable model ID. “ +
79
- “Falling back to internal UUID as litellm_id. “ +
80
- “Subsequent deregisterModel() calls for this model will likely fail silently, “ +
81
- “leaving a ghost model active inside LiteLLM. “ +
82
- “Inspect responseData below and verify your LiteLLM version.”,
83
- {
84
- modelName: model.name,
85
- internalId: model.id,
86
- responseTopLevelKeys: response.data ? Object.keys(response.data) : [],
87
- responseData: response.data,
88
- }
89
- );
90
- // Retain fallback so createModel() still returns a usable record.
91
- return model.id;
92
- }
93
 
94
- logger.info(Model registered in LiteLLM, { modelName: model.name, litellmId });
95
- return litellmId;
96
  }
97
 
98
- /**
99
-
100
- - Remove a model from LiteLLM.
101
- -
102
- - @param {string} litellmId - LiteLLM’s model ID (returned from registerModel)
103
- -
104
- - ─── BUG FIX #6: /model/delete field-name version incompatibility ──────────
105
- -
106
- - ORIGINAL CODE:
107
- - await client.post(”/model/delete”, { id: litellmId });
108
- -
109
- - The request-body field name accepted by /model/delete has changed across
110
- - LiteLLM releases:
111
- -
112
- - • Older versions (pre-v1.x): { model_id: “<id>” }
113
- - • Current versions (v1.x+): { id: “<id>” }
114
- -
115
- - Sending only `id` against an older deployment produces a silent no-op:
116
- - LiteLLM returns HTTP 200 but ignores the request because it only reads
117
- - `model_id`. The same failure mode applies in reverse on newer versions.
118
- -
119
- - FIX: Send BOTH fields in every request. LiteLLM’s Pydantic models use
120
- - `model_config = ConfigDict(extra="ignore")`, so unknown keys are silently
121
- - discarded — the payload is safe for all known versions.
122
- -
123
- - If deletion silently fails after a future LiteLLM upgrade, verify the
124
- - current accepted field name via the running instance’s Swagger UI:
125
- - http://<litellm-host>:4000/docs → POST /model/delete
126
- - ─────────────────────────────────────────────────────────────────────────
127
- */
128
- async function deregisterModel(litellmId) {
129
  if (!litellmId) return;
130
  try {
131
- await client.post(/model/delete, {
132
- id: litellmId, // accepted by LiteLLM v1.x+
133
- model_id: litellmId, // accepted by LiteLLM pre-v1.x
134
- });
135
- logger.info(Model deregistered from LiteLLM, { litellmId });
136
  } catch (err) {
137
- // Model may not exist in LiteLLM (e.g. was never synced, or litellmId
138
- // is our own UUID fallback from Bug Fix #7). Log at warn so the operator
139
- // is aware but the delete flow is not blocked.
140
- logger.warn(“Could not deregister model from LiteLLM”, {
141
- litellmId,
142
- httpStatus: err.response?.status,
143
- error: err.message,
144
- });
145
- }
146
- }
147
-
148
- /**
149
-
150
- - List all models currently registered in LiteLLM.
151
- */
152
- async function listLitellmModels() {
153
- const response = await client.get(”/model/info”);
154
- return response.data?.data || response.data || [];
155
  }
 
156
 
157
- /**
 
 
 
158
 
159
- - Update a model in LiteLLM (delete + re-add since update isn’t atomic).
160
- */
161
- async function updateModel(oldLitellmId, model) {
162
  await deregisterModel(oldLitellmId);
163
  return registerModel(model);
164
- }
165
-
166
- /**
167
 
168
- - Check LiteLLM liveness.
169
- -
170
- - Uses /health/liveliness instead of /health:
171
- - - /health validates all registered model upstreams. If any upstream is
172
- - unreachable it returns an error even though LiteLLM itself is healthy,
173
- - causing syncModelsToLitellmWithRetry() to retry endlessly and give up.
174
- - - /health/liveliness only checks that the LiteLLM process is alive, which
175
- - is the correct signal for “ready to accept /model/new requests”.
176
- - This also matches the docker-compose.yml container healthcheck target.
177
- */
178
- async function healthCheck() {
179
- const response = await client.get(”/health/liveliness”);
180
  return response.data;
181
- }
182
-
183
- /**
184
 
185
- - Test a model by sending a minimal chat completion request.
186
- */
187
- async function testModel(modelName, options = {}) {
188
  const start = Date.now();
189
  const messages =
190
- options.messages && options.messages.length > 0
191
- ? options.messages
192
- : [{ role: user, content: options.prompt || Say OK in one word. }];
193
  try {
194
- const response = await client.post(
195
- /v1/chat/completions,
196
- {
197
- model: modelName,
198
- messages,
199
- max_tokens: 256,
200
- stream: false,
201
- },
202
- { timeout: 30000 }
203
- );
204
- return {
205
- success: true,
206
- latencyMs: Date.now() - start,
207
- response: response.data,
208
- };
209
  } catch (err) {
210
- return {
211
- success: false,
212
- latencyMs: Date.now() - start,
213
- error: err.response?.data || err.message,
214
- };
215
  }
216
- }
217
-
218
- // ─── Helpers ──────────────────────────────────────────────────────────────
219
 
220
  function buildLitellmParams(model) {
221
- const params = {
222
- model: model.litellmModel,
223
- };
224
-
225
- // Note: original code also checked `model._apiBase` which is never set
226
- // anywhere in the codebase — that dead reference has been removed.
227
- if (model.apiBase) {
228
- params.api_base = model.apiBase;
229
- }
230
 
231
- // Only include api_key if provided and non-empty
232
- const key = model._apiKey || model.apiKey;
233
- if (key && key !== “••••••••” && key.trim() !== “”) {
234
- params.api_key = key.trim();
235
- } else {
236
- // LiteLLM requires some api_key for most providers; use placeholder
237
- params.api_key = “none”;
238
- }
 
 
239
 
240
- return params;
241
  }
242
 
243
  module.exports = {
244
- registerModel,
245
- deregisterModel,
246
- listLitellmModels,
247
- updateModel,
248
- healthCheck,
249
- testModel,
250
  };
 
1
  "use strict";
2
 
3
  /**
4
+ * LiteLLM Proxy Management Client
5
+ * Wraps LiteLLM admin API for dynamic model registration.
6
+ */
7
 
8
+ const axios = require("axios");
9
+ const { logger } = require("./logger");
 
10
 
11
+ const LITELLM_BASE_URL = process.env.LITELLM_BASE_URL || "http://litellm:4000";
12
+ const LITELLM_MASTER_KEY = process.env.LITELLM_MASTER_KEY || "sk-gateway-master-key";
 
 
 
13
 
14
  const client = axios.create({
15
+ baseURL: LITELLM_BASE_URL,
16
+ timeout: 15000,
17
+ headers: {
18
+ Authorization: "Bearer " + LITELLM_MASTER_KEY,
19
+ "Content-Type": "application/json",
20
+ },
21
  });
22
 
23
+ async function registerModel(model) {
 
 
 
 
 
 
 
 
24
  const payload = {
25
+ model_name: model.name,
26
+ litellm_params: buildLitellmParams(model),
27
+ model_info: {
28
+ id: model.id,
29
+ description: model.description || "",
30
+ model_type: model.modelType || "chat",
31
+ },
32
  };
33
 
34
+ logger.info("Registering model with LiteLLM", { modelName: model.name });
35
+
36
+ const response = await client.post("/model/new", payload);
37
+
38
+ const litellmId =
39
+ (response.data && response.data.model_info && response.data.model_info.id) ||
40
+ (response.data && response.data.id) ||
41
+ null;
42
+
43
+ if (!litellmId) {
44
+ logger.warn(
45
+ "LiteLLM /model/new response contained no recognisable model ID. " +
46
+ "Falling back to internal UUID. " +
47
+ "Deregistration may fail silently leaving a ghost model in LiteLLM.",
48
+ {
49
+ modelName: model.name,
50
+ internalId: model.id,
51
+ responseTopLevelKeys: response.data ? Object.keys(response.data) : [],
52
+ responseData: response.data,
53
+ }
54
+ );
55
+ return model.id;
56
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
 
58
+ logger.info("Model registered in LiteLLM", { modelName: model.name, litellmId: litellmId });
59
+ return litellmId;
60
  }
61
 
62
+ async function deregisterModel(litellmId) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  if (!litellmId) return;
64
  try {
65
+ await client.post("/model/delete", {
66
+ id: litellmId,
67
+ model_id: litellmId,
68
+ });
69
+ logger.info("Model deregistered from LiteLLM", { litellmId: litellmId });
70
  } catch (err) {
71
+ logger.warn("Could not deregister model from LiteLLM", {
72
+ litellmId: litellmId,
73
+ httpStatus: err.response && err.response.status,
74
+ error: err.message,
75
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  }
77
+ }
78
 
79
+ async function listLitellmModels() {
80
+ const response = await client.get("/model/info");
81
+ return (response.data && response.data.data) || response.data || [];
82
+ }
83
 
84
+ async function updateModel(oldLitellmId, model) {
 
 
85
  await deregisterModel(oldLitellmId);
86
  return registerModel(model);
87
+ }
 
 
88
 
89
+ async function healthCheck() {
90
+ const response = await client.get("/health/liveliness");
 
 
 
 
 
 
 
 
 
 
91
  return response.data;
92
+ }
 
 
93
 
94
+ async function testModel(modelName, options) {
95
+ if (!options) { options = {}; }
 
96
  const start = Date.now();
97
  const messages =
98
+ options.messages && options.messages.length > 0
99
+ ? options.messages
100
+ : [{ role: "user", content: options.prompt || "Say OK in one word." }];
101
  try {
102
+ const response = await client.post(
103
+ "/v1/chat/completions",
104
+ {
105
+ model: modelName,
106
+ messages: messages,
107
+ max_tokens: 256,
108
+ stream: false,
109
+ },
110
+ { timeout: 30000 }
111
+ );
112
+ return {
113
+ success: true,
114
+ latencyMs: Date.now() - start,
115
+ response: response.data,
116
+ };
117
  } catch (err) {
118
+ return {
119
+ success: false,
120
+ latencyMs: Date.now() - start,
121
+ error: (err.response && err.response.data) || err.message,
122
+ };
123
  }
124
+ }
 
 
125
 
126
  function buildLitellmParams(model) {
127
+ const params = {
128
+ model: model.litellmModel,
129
+ };
 
 
 
 
 
 
130
 
131
+ if (model.apiBase) {
132
+ params.api_base = model.apiBase;
133
+ }
134
+
135
+ const key = model._apiKey || model.apiKey;
136
+ if (key && key !== "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" && key.trim() !== "") {
137
+ params.api_key = key.trim();
138
+ } else {
139
+ params.api_key = "none";
140
+ }
141
 
142
+ return params;
143
  }
144
 
145
  module.exports = {
146
+ registerModel: registerModel,
147
+ deregisterModel: deregisterModel,
148
+ listLitellmModels: listLitellmModels,
149
+ updateModel: updateModel,
150
+ healthCheck: healthCheck,
151
+ testModel: testModel,
152
  };