plugin-ai-api 1.0.21 → 1.0.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/client/123.e6fe04c856ce6417.js +10 -0
  2. package/dist/client/902.e74518750f1e4201.js +10 -0
  3. package/dist/client/index.js +1 -1
  4. package/dist/client-v2/123.05f1f649923f93eb.js +10 -0
  5. package/dist/client-v2/902.c7c00a565085438a.js +10 -0
  6. package/dist/client-v2/index.js +1 -1
  7. package/dist/constants.js +5 -2
  8. package/dist/locale/en-US.json +15 -1
  9. package/dist/locale/vi-VN.json +15 -1
  10. package/dist/locale/zh-CN.json +15 -1
  11. package/dist/server/collections/ai-api-user-permissions.js +67 -0
  12. package/dist/server/collections/ai-api-user-quota-policies.js +2 -1
  13. package/dist/server/plugin.js +32 -0
  14. package/dist/server/resource/ai-api-user-permissions.js +75 -0
  15. package/dist/server/routes/agent-completions.js +5 -0
  16. package/dist/server/routes/chat-completions.js +52 -27
  17. package/dist/server/routes/completions.js +59 -33
  18. package/dist/server/routes/embeddings.js +6 -14
  19. package/dist/server/routes/models.js +24 -0
  20. package/dist/server/utils/direct-llm-context.js +184 -0
  21. package/dist/server/utils/openai-format.js +17 -3
  22. package/dist/server/utils/user-permissions.js +160 -0
  23. package/dist/server/validation.js +3 -0
  24. package/dist/swagger.js +4 -3
  25. package/package.json +2 -2
  26. package/src/client/__tests__/settings-registration.test.tsx +69 -0
  27. package/src/client/plugin.tsx +14 -3
  28. package/src/client-v2/__tests__/settings-registration.test.tsx +33 -4
  29. package/src/client-v2/pages/UserPermissionsPage.tsx +322 -0
  30. package/src/client-v2/pages/UserQuotasPage.tsx +18 -0
  31. package/src/client-v2/plugin.tsx +12 -3
  32. package/src/constants.ts +7 -0
  33. package/src/locale/en-US.json +15 -1
  34. package/src/locale/vi-VN.json +15 -1
  35. package/src/locale/zh-CN.json +15 -1
  36. package/src/server/__tests__/direct-llm-context.test.ts +125 -0
  37. package/src/server/__tests__/models.test.ts +44 -2
  38. package/src/server/__tests__/openai-format.test.ts +52 -1
  39. package/src/server/__tests__/permission-sync.test.ts +109 -0
  40. package/src/server/__tests__/usage-route.test.ts +265 -5
  41. package/src/server/__tests__/user-permissions-resource.test.ts +66 -0
  42. package/src/server/__tests__/user-permissions.test.ts +284 -0
  43. package/src/server/__tests__/validation.test.ts +36 -0
  44. package/src/server/collections/ai-api-user-permissions.ts +46 -0
  45. package/src/server/collections/ai-api-user-quota-policies.ts +1 -0
  46. package/src/server/plugin.ts +42 -1
  47. package/src/server/resource/ai-api-user-permissions.ts +76 -0
  48. package/src/server/routes/agent-completions.ts +7 -0
  49. package/src/server/routes/chat-completions.ts +58 -30
  50. package/src/server/routes/completions.ts +68 -34
  51. package/src/server/routes/embeddings.ts +10 -15
  52. package/src/server/routes/models.ts +28 -0
  53. package/src/server/utils/direct-llm-context.ts +216 -0
  54. package/src/server/utils/openai-format.ts +26 -0
  55. package/src/server/utils/user-permissions.ts +218 -0
  56. package/src/server/validation.ts +3 -0
  57. package/src/swagger.ts +9 -3
  58. package/dist/client/902.92e1daaf1ab16ebf.js +0 -10
  59. package/dist/client-v2/902.9054d990ddc223ac.js +0 -10
@@ -38,8 +38,10 @@ var import_openai_format = require("../utils/openai-format");
38
38
  var import_resolve_service = require("../utils/resolve-service");
39
39
  var import_streaming = require("../utils/streaming");
40
40
  var import_role_permission = require("../middleware/role-permission");
41
+ var import_user_permissions = require("../utils/user-permissions");
41
42
  var import_usage = require("../usage");
42
43
  var import_billing = require("../billing");
44
+ var import_direct_llm_context = require("../utils/direct-llm-context");
43
45
  var import_app_observability = require("../utils/app-observability");
44
46
  async function handleChatCompletions(ctx, plugin) {
45
47
  var _a;
@@ -117,20 +119,8 @@ async function handleChatCompletions(ctx, plugin) {
117
119
  return;
118
120
  }
119
121
  const config = await ctx.db.getRepository("aiApiConfig").findOne();
120
- if ((_a = config == null ? void 0 : config.enabledLlmServices) == null ? void 0 : _a.length) {
121
- const serviceName = service.name;
122
- const serviceTitle = service.title;
123
- const isAllowed = config.enabledLlmServices.some((s) => s === serviceName || s === serviceTitle);
124
- if (!isAllowed) {
125
- ctx.status = 403;
126
- ctx.body = (0, import_openai_format.toOpenAIError)(
127
- 403,
128
- `LLM service '${service.title || service.name}' is not enabled for API access`,
129
- "invalid_request_error",
130
- "model_not_available"
131
- );
132
- return;
133
- }
122
+ if (!await (0, import_user_permissions.enforceModelAccess)(ctx, config == null ? void 0 : config.enabledLlmServices, service, modelId)) {
123
+ return;
134
124
  }
135
125
  const providerMeta = aiPlugin.aiManager.llmProviders.get(service.provider);
136
126
  if (!providerMeta) {
@@ -138,8 +128,14 @@ async function handleChatCompletions(ctx, plugin) {
138
128
  ctx.body = (0, import_openai_format.toOpenAIError)(500, `Provider '${service.provider}' not registered`, "server_error");
139
129
  return;
140
130
  }
141
- await (0, import_billing.prepareLlmBilling)(ctx, resolved);
142
131
  const providerRequestParameters = getProviderRequestParameters(body);
132
+ if (stream) {
133
+ const streamOptions = isRecord(body.stream_options) ? body.stream_options : {};
134
+ providerRequestParameters.stream_options = {
135
+ ...streamOptions,
136
+ include_usage: true
137
+ };
138
+ }
143
139
  const modelOptions = {
144
140
  model: modelId,
145
141
  llmService: service.name
@@ -151,12 +147,6 @@ async function handleChatCompletions(ctx, plugin) {
151
147
  if (body.frequency_penalty !== void 0) modelOptions.frequencyPenalty = body.frequency_penalty;
152
148
  if (body.presence_penalty !== void 0) modelOptions.presencePenalty = body.presence_penalty;
153
149
  if (body.stop !== void 0) modelOptions.stop = body.stop;
154
- const Provider = providerMeta.provider;
155
- const provider = new Provider({
156
- app: ctx.app,
157
- serviceOptions: service.options,
158
- modelOptions
159
- });
160
150
  let systemPrompt = "";
161
151
  if (config == null ? void 0 : config.defaultAiEmployee) {
162
152
  if (!(0, import_role_permission.checkEmployeeAccess)(ctx, config.defaultAiEmployee)) {
@@ -176,11 +166,27 @@ async function handleChatCompletions(ctx, plugin) {
176
166
  systemPrompt = employee.about || employee.defaultPrompt || "";
177
167
  }
178
168
  }
179
- const messages = [...body.messages];
169
+ let messages = [...body.messages];
180
170
  const hasSystemMessage = messages.some((m) => m.role === "system");
181
171
  if (systemPrompt && !hasSystemMessage) {
182
172
  messages.unshift({ role: "system", content: systemPrompt });
183
173
  }
174
+ const preparedContext = await (0, import_direct_llm_context.prepareDirectLlmContext)(ctx, {
175
+ serviceName: service.name,
176
+ modelId,
177
+ messages,
178
+ tools: body.tools,
179
+ maxCompletionTokens: body.max_completion_tokens,
180
+ maxTokens: body.max_tokens
181
+ });
182
+ messages = preparedContext.messages;
183
+ await (0, import_billing.prepareLlmBilling)(ctx, resolved);
184
+ const Provider = providerMeta.provider;
185
+ const provider = new Provider({
186
+ app: ctx.app,
187
+ serviceOptions: service.options,
188
+ modelOptions
189
+ });
184
190
  const langchainMessages = messages.map((msg) => {
185
191
  const role = msg.role === "assistant" ? "ai" : msg.role;
186
192
  const content = normalizeMessageContent(msg.content);
@@ -223,15 +229,16 @@ async function handleChatCompletions(ctx, plugin) {
223
229
  }
224
230
  } catch (err) {
225
231
  ctx.log.error("AI API chat completions error:", err);
226
- if (!ctx.res.headersSent) {
232
+ if (!((_a = ctx.res) == null ? void 0 : _a.headersSent)) {
227
233
  const isQuotaError = err instanceof import_billing.AiApiQuotaError;
228
- ctx.status = isQuotaError ? 429 : 500;
234
+ const isContextError = err instanceof import_direct_llm_context.DirectLlmContextError;
235
+ ctx.status = isQuotaError ? 429 : isContextError ? 400 : 500;
229
236
  if (isQuotaError) ctx.set("X-RateLimit-Reason", err.code);
230
237
  ctx.body = (0, import_openai_format.toOpenAIError)(
231
238
  ctx.status,
232
239
  getErrorMessage(err, "Internal server error"),
233
- isQuotaError ? "quota_error" : "server_error",
234
- isQuotaError ? err.code : void 0
240
+ isQuotaError ? "quota_error" : isContextError ? "invalid_request_error" : "server_error",
241
+ isQuotaError || isContextError ? err.code : void 0
235
242
  );
236
243
  }
237
244
  }
@@ -312,7 +319,13 @@ async function handleStreamingCompletion(ctx, chatModel, messages, completionId,
312
319
  finishReason = "tool_calls";
313
320
  await (0, import_streaming.writeResponse)(
314
321
  ctx,
315
- (0, import_openai_format.formatSSE)((0, import_openai_format.toOpenAIStreamChunk)({ id: completionId, model: modelName, delta: { tool_calls: toolCallChunks } }))
322
+ (0, import_openai_format.formatSSE)(
323
+ (0, import_openai_format.toOpenAIStreamChunk)({
324
+ id: completionId,
325
+ model: modelName,
326
+ delta: { tool_calls: toolCallChunks }
327
+ })
328
+ )
316
329
  );
317
330
  }
318
331
  if (chunk.usage_metadata) {
@@ -331,6 +344,18 @@ async function handleStreamingCompletion(ctx, chatModel, messages, completionId,
331
344
  })
332
345
  )
333
346
  );
347
+ if (usage) {
348
+ await (0, import_streaming.writeResponse)(
349
+ ctx,
350
+ (0, import_openai_format.formatSSE)(
351
+ (0, import_openai_format.toOpenAIUsageChunk)({
352
+ id: completionId,
353
+ model: modelName,
354
+ usage
355
+ })
356
+ )
357
+ );
358
+ }
334
359
  await (0, import_streaming.writeResponse)(ctx, (0, import_openai_format.formatSSEDone)());
335
360
  (0, import_usage.setAiApiUsageResult)(ctx, usage, { gatewayResponseId: completionId, providerRequestId });
336
361
  ctx.state.aiApiStreamResult = { succeeded: true, id: completionId };
@@ -31,9 +31,11 @@ __export(completions_exports, {
31
31
  module.exports = __toCommonJS(completions_exports);
32
32
  var import_openai_format = require("../utils/openai-format");
33
33
  var import_resolve_service = require("../utils/resolve-service");
34
+ var import_user_permissions = require("../utils/user-permissions");
34
35
  var import_streaming = require("../utils/streaming");
35
36
  var import_usage = require("../usage");
36
37
  var import_billing = require("../billing");
38
+ var import_direct_llm_context = require("../utils/direct-llm-context");
37
39
  var import_app_observability = require("../utils/app-observability");
38
40
  async function handleCompletions(ctx, plugin) {
39
41
  var _a;
@@ -89,20 +91,8 @@ async function handleCompletions(ctx, plugin) {
89
91
  return;
90
92
  }
91
93
  const config = await ctx.db.getRepository("aiApiConfig").findOne();
92
- if ((_a = config == null ? void 0 : config.enabledLlmServices) == null ? void 0 : _a.length) {
93
- const serviceName = service.name;
94
- const serviceTitle = service.title;
95
- const isAllowed = config.enabledLlmServices.some((s) => s === serviceName || s === serviceTitle);
96
- if (!isAllowed) {
97
- ctx.status = 403;
98
- ctx.body = (0, import_openai_format.toOpenAIError)(
99
- 403,
100
- `LLM service '${service.title || service.name}' is not enabled for API access`,
101
- "invalid_request_error",
102
- "model_not_available"
103
- );
104
- return;
105
- }
94
+ if (!await (0, import_user_permissions.enforceModelAccess)(ctx, config == null ? void 0 : config.enabledLlmServices, service, modelId)) {
95
+ return;
106
96
  }
107
97
  const providerMeta = aiPlugin.aiManager.llmProviders.get(service.provider);
108
98
  if (!providerMeta) {
@@ -110,7 +100,6 @@ async function handleCompletions(ctx, plugin) {
110
100
  ctx.body = (0, import_openai_format.toOpenAIError)(500, `Provider '${service.provider}' not registered`, "server_error");
111
101
  return;
112
102
  }
113
- await (0, import_billing.prepareLlmBilling)(ctx, resolved);
114
103
  const modelOptions = {
115
104
  model: modelId,
116
105
  llmService: service.name
@@ -119,14 +108,8 @@ async function handleCompletions(ctx, plugin) {
119
108
  if (body.top_p !== void 0) modelOptions.topP = body.top_p;
120
109
  if (body.max_tokens !== void 0) modelOptions.maxTokens = body.max_tokens;
121
110
  if (body.stop !== void 0) modelOptions.stop = body.stop;
122
- const Provider = providerMeta.provider;
123
- const provider = new Provider({
124
- app: ctx.app,
125
- serviceOptions: service.options,
126
- modelOptions
127
- });
128
111
  const prompt = typeof body.prompt === "string" ? body.prompt : Array.isArray(body.prompt) ? body.prompt.join("\n") : String(body.prompt);
129
- const langchainMessages = [];
112
+ const messages = [];
130
113
  if (config == null ? void 0 : config.defaultAiEmployee) {
131
114
  const employee = await ctx.db.getRepository("aiEmployees").findOne({
132
115
  filter: { username: config.defaultAiEmployee }
@@ -134,30 +117,55 @@ async function handleCompletions(ctx, plugin) {
134
117
  if (employee) {
135
118
  const systemPrompt = employee.about || employee.defaultPrompt || "";
136
119
  if (systemPrompt) {
137
- langchainMessages.push(["system", systemPrompt]);
120
+ messages.push({ role: "system", content: systemPrompt });
138
121
  }
139
122
  }
140
123
  }
141
- langchainMessages.push(["human", prompt]);
124
+ messages.push({ role: "user", content: prompt });
125
+ const preparedContext = await (0, import_direct_llm_context.prepareDirectLlmContext)(ctx, {
126
+ serviceName: service.name,
127
+ modelId,
128
+ messages,
129
+ maxTokens: body.max_tokens
130
+ });
131
+ await (0, import_billing.prepareLlmBilling)(ctx, resolved);
132
+ const Provider = providerMeta.provider;
133
+ const provider = new Provider({
134
+ app: ctx.app,
135
+ serviceOptions: service.options,
136
+ modelOptions
137
+ });
138
+ const langchainMessages = preparedContext.messages.map((message) => [
139
+ message.role === "user" ? "human" : message.role,
140
+ String(message.content ?? "")
141
+ ]);
142
142
  const completionId = (0, import_openai_format.generateCompletionId)().replace("chatcmpl-", "cmpl-");
143
143
  const chatModel = provider.createModel();
144
144
  (0, import_billing.markLlmProviderAttempted)(ctx);
145
145
  if (stream) {
146
- await handleStreamingTextCompletion(ctx, chatModel, langchainMessages, completionId, body.model);
146
+ await handleStreamingTextCompletion(
147
+ ctx,
148
+ chatModel,
149
+ langchainMessages,
150
+ completionId,
151
+ body.model,
152
+ body.stream_options
153
+ );
147
154
  } else {
148
155
  await handleNonStreamingTextCompletion(ctx, chatModel, langchainMessages, completionId, body.model);
149
156
  }
150
157
  } catch (err) {
151
158
  ctx.log.error("AI API completions error:", err);
152
- if (!ctx.res.headersSent) {
159
+ if (!((_a = ctx.res) == null ? void 0 : _a.headersSent)) {
153
160
  const isQuotaError = err instanceof import_billing.AiApiQuotaError;
154
- ctx.status = isQuotaError ? 429 : 500;
161
+ const isContextError = err instanceof import_direct_llm_context.DirectLlmContextError;
162
+ ctx.status = isQuotaError ? 429 : isContextError ? 400 : 500;
155
163
  if (isQuotaError) ctx.set("X-RateLimit-Reason", err.code);
156
164
  ctx.body = (0, import_openai_format.toOpenAIError)(
157
165
  ctx.status,
158
166
  getErrorMessage(err, "Internal server error"),
159
- isQuotaError ? "quota_error" : "server_error",
160
- isQuotaError ? err.code : void 0
167
+ isQuotaError ? "quota_error" : isContextError ? "invalid_request_error" : "server_error",
168
+ isQuotaError || isContextError ? err.code : void 0
161
169
  );
162
170
  }
163
171
  }
@@ -193,7 +201,7 @@ async function handleNonStreamingTextCompletion(ctx, chatModel, messages, comple
193
201
  usage: usage ?? { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
194
202
  };
195
203
  }
196
- async function handleStreamingTextCompletion(ctx, chatModel, messages, completionId, modelName) {
204
+ async function handleStreamingTextCompletion(ctx, chatModel, messages, completionId, modelName, streamOptions) {
197
205
  ctx.set({
198
206
  "Content-Type": "text/event-stream",
199
207
  "Cache-Control": "no-cache",
@@ -205,7 +213,10 @@ async function handleStreamingTextCompletion(ctx, chatModel, messages, completio
205
213
  let usage;
206
214
  let providerRequestId;
207
215
  try {
208
- const stream = await chatModel.stream(messages, { signal: requestAbort.signal });
216
+ const stream = await chatModel.stream(messages, {
217
+ stream_options: { ...streamOptions, include_usage: true },
218
+ signal: requestAbort.signal
219
+ });
209
220
  for await (const chunk of stream) {
210
221
  if (requestAbort.signal.aborted) throw requestAbort.signal.reason;
211
222
  let text = "";
@@ -232,7 +243,8 @@ async function handleStreamingTextCompletion(ctx, chatModel, messages, completio
232
243
  logprobs: null,
233
244
  finish_reason: null
234
245
  }
235
- ]
246
+ ],
247
+ usage: null
236
248
  })
237
249
  );
238
250
  }
@@ -256,9 +268,23 @@ async function handleStreamingTextCompletion(ctx, chatModel, messages, completio
256
268
  logprobs: null,
257
269
  finish_reason: "stop"
258
270
  }
259
- ]
271
+ ],
272
+ usage: null
260
273
  })
261
274
  );
275
+ if (usage) {
276
+ await (0, import_streaming.writeResponse)(
277
+ ctx,
278
+ (0, import_openai_format.formatSSE)(
279
+ (0, import_openai_format.toOpenAIUsageChunk)({
280
+ id: completionId,
281
+ model: modelName,
282
+ object: "text_completion",
283
+ usage
284
+ })
285
+ )
286
+ );
287
+ }
262
288
  await (0, import_streaming.writeResponse)(ctx, (0, import_openai_format.formatSSEDone)());
263
289
  (0, import_usage.setAiApiUsageResult)(ctx, usage, { gatewayResponseId: completionId, providerRequestId });
264
290
  ctx.state.aiApiStreamResult = { succeeded: true, id: completionId };
@@ -31,9 +31,9 @@ __export(embeddings_exports, {
31
31
  module.exports = __toCommonJS(embeddings_exports);
32
32
  var import_openai_format = require("../utils/openai-format");
33
33
  var import_resolve_service = require("../utils/resolve-service");
34
+ var import_user_permissions = require("../utils/user-permissions");
34
35
  var import_usage = require("../usage");
35
36
  async function handleEmbeddings(ctx, plugin) {
36
- var _a;
37
37
  const body = ctx.request.body;
38
38
  if (!(body == null ? void 0 : body.model)) {
39
39
  ctx.status = 400;
@@ -102,23 +102,15 @@ async function handleEmbeddings(ctx, plugin) {
102
102
  );
103
103
  return;
104
104
  }
105
+ let globalEnabledServices = [];
105
106
  try {
106
107
  const config = await ctx.db.getRepository("aiApiConfig").findOne();
107
- if ((_a = config == null ? void 0 : config.enabledLlmServices) == null ? void 0 : _a.length) {
108
- const allowed = config.enabledLlmServices.some((s) => s === service.name || s === service.title);
109
- if (!allowed) {
110
- ctx.status = 403;
111
- ctx.body = (0, import_openai_format.toOpenAIError)(
112
- 403,
113
- `LLM service '${service.title || service.name}' is not enabled for API access`,
114
- "invalid_request_error",
115
- "model_not_available"
116
- );
117
- return;
118
- }
119
- }
108
+ globalEnabledServices = (config == null ? void 0 : config.enabledLlmServices) ?? [];
120
109
  } catch {
121
110
  }
111
+ if (!await (0, import_user_permissions.enforceModelAccess)(ctx, globalEnabledServices, service, modelId)) {
112
+ return;
113
+ }
122
114
  const aiPlugin = ctx.app.pm.get("ai");
123
115
  if (!aiPlugin) {
124
116
  ctx.status = 500;
@@ -32,6 +32,7 @@ __export(models_exports, {
32
32
  });
33
33
  module.exports = __toCommonJS(models_exports);
34
34
  var import_openai_format = require("../utils/openai-format");
35
+ var import_user_permissions = require("../utils/user-permissions");
35
36
  async function handleListModels(ctx, plugin) {
36
37
  var _a;
37
38
  try {
@@ -50,15 +51,22 @@ async function handleListModels(ctx, plugin) {
50
51
  filter,
51
52
  sort: "sort"
52
53
  });
54
+ const scope = await (0, import_user_permissions.resolveUserAccessScope)(ctx);
55
+ if (scope.lookupFailed) {
56
+ respondPermissionCheckFailed(ctx);
57
+ return;
58
+ }
53
59
  const metadataMap = await loadModelMetadata(ctx);
54
60
  const now = Math.floor(Date.now() / 1e3);
55
61
  const models = [];
56
62
  for (const service of services) {
57
63
  if (service.enabled === false) continue;
64
+ if (!(0, import_user_permissions.isServiceAllowed)(scope, config == null ? void 0 : config.enabledLlmServices, service)) continue;
58
65
  const enabledModels = resolveEnabledModels(service);
59
66
  const serviceLabel = service.title || service.name;
60
67
  for (const model of enabledModels) {
61
68
  const fullId = `${service.name}/${model.value}`;
69
+ if (!(0, import_user_permissions.isModelAllowed)(scope, fullId)) continue;
62
70
  const meta = metadataMap.get(fullId);
63
71
  if (meta && meta.enabled === false) continue;
64
72
  models.push(buildModelObject(fullId, now, serviceLabel, meta));
@@ -87,16 +95,23 @@ async function handleGetModel(ctx, modelId, plugin) {
87
95
  filter,
88
96
  sort: "sort"
89
97
  });
98
+ const scope = await (0, import_user_permissions.resolveUserAccessScope)(ctx);
99
+ if (scope.lookupFailed) {
100
+ respondPermissionCheckFailed(ctx);
101
+ return;
102
+ }
90
103
  const metadataMap = await loadModelMetadata(ctx);
91
104
  const now = Math.floor(Date.now() / 1e3);
92
105
  let found = null;
93
106
  for (const service of services) {
94
107
  if (service.enabled === false) continue;
108
+ if (!(0, import_user_permissions.isServiceAllowed)(scope, config == null ? void 0 : config.enabledLlmServices, service)) continue;
95
109
  const enabledModels = resolveEnabledModels(service);
96
110
  const serviceLabel = service.title || service.name;
97
111
  for (const model of enabledModels) {
98
112
  const fullId = `${service.name}/${model.value}`;
99
113
  if (fullId === modelId || model.value === modelId) {
114
+ if (!(0, import_user_permissions.isModelAllowed)(scope, fullId)) continue;
100
115
  const meta = metadataMap.get(fullId);
101
116
  if (meta && meta.enabled === false) continue;
102
117
  found = buildModelObject(fullId, now, serviceLabel, meta);
@@ -118,6 +133,15 @@ async function handleGetModel(ctx, modelId, plugin) {
118
133
  ctx.body = (0, import_openai_format.toOpenAIError)(500, "Failed to retrieve model", "server_error");
119
134
  }
120
135
  }
136
+ function respondPermissionCheckFailed(ctx) {
137
+ ctx.status = 503;
138
+ ctx.body = (0, import_openai_format.toOpenAIError)(
139
+ 503,
140
+ "Unable to verify LLM permissions for this user. Please retry shortly.",
141
+ "service_unavailable",
142
+ "permission_check_failed"
143
+ );
144
+ }
121
145
  async function loadModelMetadata(ctx) {
122
146
  var _a, _b;
123
147
  const map = /* @__PURE__ */ new Map();
@@ -0,0 +1,184 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ var __defProp = Object.defineProperty;
11
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
12
+ var __getOwnPropNames = Object.getOwnPropertyNames;
13
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
14
+ var __export = (target, all) => {
15
+ for (var name in all)
16
+ __defProp(target, name, { get: all[name], enumerable: true });
17
+ };
18
+ var __copyProps = (to, from, except, desc) => {
19
+ if (from && typeof from === "object" || typeof from === "function") {
20
+ for (let key of __getOwnPropNames(from))
21
+ if (!__hasOwnProp.call(to, key) && key !== except)
22
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
23
+ }
24
+ return to;
25
+ };
26
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
27
+ var direct_llm_context_exports = {};
28
+ __export(direct_llm_context_exports, {
29
+ DirectLlmContextError: () => DirectLlmContextError,
30
+ prepareDirectLlmContext: () => prepareDirectLlmContext
31
+ });
32
+ module.exports = __toCommonJS(direct_llm_context_exports);
33
+ class DirectLlmContextError extends Error {
34
+ constructor(code, message) {
35
+ super(message);
36
+ this.code = code;
37
+ this.name = "DirectLlmContextError";
38
+ }
39
+ }
40
+ function getValue(record, key) {
41
+ var _a;
42
+ return ((_a = record == null ? void 0 : record.get) == null ? void 0 : _a.call(record, key)) ?? (record == null ? void 0 : record[key]);
43
+ }
44
+ function positiveInteger(value) {
45
+ const parsed = Number(value);
46
+ return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : void 0;
47
+ }
48
+ function hasImageContent(value) {
49
+ return Array.isArray(value) && value.some(
50
+ (block) => typeof block === "object" && block !== null && block.type === "image_url"
51
+ );
52
+ }
53
+ function estimateValueTokens(value) {
54
+ if (value === void 0) return 0;
55
+ return Math.ceil(Buffer.byteLength(JSON.stringify(value), "utf8") / 3);
56
+ }
57
+ function estimateMessagesTokens(messages) {
58
+ return messages.reduce((total, message) => total + estimateValueTokens(message) + 4, 0);
59
+ }
60
+ function containsUnsupportedContent(messages) {
61
+ return messages.some((message) => hasImageContent(message.content));
62
+ }
63
+ function isInstruction(message) {
64
+ return message.role === "system" || message.role === "developer";
65
+ }
66
+ function splitTurns(messages) {
67
+ const fixed = [];
68
+ const turns = [];
69
+ let currentTurn;
70
+ for (const message of messages) {
71
+ if (isInstruction(message)) {
72
+ fixed.push(message);
73
+ continue;
74
+ }
75
+ if (message.role === "user" || !currentTurn) {
76
+ currentTurn = [message];
77
+ turns.push(currentTurn);
78
+ continue;
79
+ }
80
+ currentTurn.push(message);
81
+ }
82
+ return { fixed, turns };
83
+ }
84
+ function messagesWithTurns(messages, turns) {
85
+ const retainedMessages = new Set(turns.flat());
86
+ return messages.filter((message) => isInstruction(message) || retainedMessages.has(message));
87
+ }
88
+ async function loadModelMetadata(ctx, serviceName, modelId) {
89
+ const row = await ctx.db.getRepository("aiApiModelMetadata").findOne({
90
+ filter: { llmService: serviceName, model: modelId, enabled: true }
91
+ });
92
+ const contextWindow = positiveInteger(getValue(row, "contextWindow"));
93
+ const maxCompletionTokens = positiveInteger(getValue(row, "maxCompletionTokens"));
94
+ if (!contextWindow || !maxCompletionTokens) {
95
+ throw new DirectLlmContextError(
96
+ "model_context_metadata_not_configured",
97
+ `Context metadata is not configured for '${serviceName}/${modelId}'. Configure context window and max completion tokens.`
98
+ );
99
+ }
100
+ return { contextWindow, maxCompletionTokens };
101
+ }
102
+ async function resolveOverflowBehavior(ctx) {
103
+ var _a;
104
+ const userId = (_a = ctx.state.currentUser) == null ? void 0 : _a.id;
105
+ if (userId === null || userId === void 0) return "reject";
106
+ const policy = await ctx.db.getRepository("aiApiUserQuotaPolicies").findOne({
107
+ filter: { userId, enabled: true }
108
+ });
109
+ return getValue(policy, "contextOverflowBehavior") === "truncate" ? "truncate" : "reject";
110
+ }
111
+ function resolveReservedOutputTokens(options, metadata) {
112
+ const requested = positiveInteger(options.maxCompletionTokens ?? options.maxTokens);
113
+ if (requested && requested > metadata.maxCompletionTokens) {
114
+ throw new DirectLlmContextError(
115
+ "max_completion_tokens_exceeds_model_limit",
116
+ `Requested max completion tokens (${requested}) exceeds the model limit (${metadata.maxCompletionTokens}).`
117
+ );
118
+ }
119
+ return requested ?? metadata.maxCompletionTokens;
120
+ }
121
+ async function prepareDirectLlmContext(ctx, options) {
122
+ if (containsUnsupportedContent(options.messages)) {
123
+ throw new DirectLlmContextError(
124
+ "context_estimation_unsupported",
125
+ "Context enforcement does not support image_url content without a model-specific vision token estimator."
126
+ );
127
+ }
128
+ const [metadata, behavior] = await Promise.all([
129
+ loadModelMetadata(ctx, options.serviceName, options.modelId),
130
+ resolveOverflowBehavior(ctx)
131
+ ]);
132
+ const reservedOutputTokens = resolveReservedOutputTokens(options, metadata);
133
+ const inputTokenBudget = metadata.contextWindow - reservedOutputTokens;
134
+ if (inputTokenBudget <= 0) {
135
+ throw new DirectLlmContextError(
136
+ "context_length_exceeded",
137
+ `The model context window (${metadata.contextWindow}) leaves no input capacity after reserving ${reservedOutputTokens} output tokens.`
138
+ );
139
+ }
140
+ const fixedOverheadTokens = estimateValueTokens(options.tools) + (options.tools === void 0 ? 0 : 4);
141
+ const originalEstimate = estimateMessagesTokens(options.messages) + fixedOverheadTokens;
142
+ if (originalEstimate <= inputTokenBudget) {
143
+ return {
144
+ messages: options.messages,
145
+ estimatedInputTokens: originalEstimate,
146
+ inputTokenBudget,
147
+ reservedOutputTokens,
148
+ truncated: false
149
+ };
150
+ }
151
+ if (behavior === "reject") {
152
+ throw new DirectLlmContextError(
153
+ "context_length_exceeded",
154
+ `Estimated input tokens (${originalEstimate}) exceed the allowed input budget (${inputTokenBudget}).`
155
+ );
156
+ }
157
+ const { turns } = splitTurns(options.messages);
158
+ let remainingTurns = turns;
159
+ let messages = messagesWithTurns(options.messages, remainingTurns);
160
+ let estimatedInputTokens = estimateMessagesTokens(messages) + fixedOverheadTokens;
161
+ while (remainingTurns.length > 1 && estimatedInputTokens > inputTokenBudget) {
162
+ remainingTurns = remainingTurns.slice(1);
163
+ messages = messagesWithTurns(options.messages, remainingTurns);
164
+ estimatedInputTokens = estimateMessagesTokens(messages) + fixedOverheadTokens;
165
+ }
166
+ if (estimatedInputTokens > inputTokenBudget) {
167
+ throw new DirectLlmContextError(
168
+ "context_length_exceeded",
169
+ `The fixed instructions, tools, and newest conversation turn require ${estimatedInputTokens} input tokens, exceeding the allowed budget (${inputTokenBudget}).`
170
+ );
171
+ }
172
+ return {
173
+ messages,
174
+ estimatedInputTokens,
175
+ inputTokenBudget,
176
+ reservedOutputTokens,
177
+ truncated: messages.length !== options.messages.length
178
+ };
179
+ }
180
+ // Annotate the CommonJS export names for ESM import in node:
181
+ 0 && (module.exports = {
182
+ DirectLlmContextError,
183
+ prepareDirectLlmContext
184
+ });
@@ -43,7 +43,8 @@ __export(openai_format_exports, {
43
43
  toOpenAIEmbeddingsResponse: () => toOpenAIEmbeddingsResponse,
44
44
  toOpenAIError: () => toOpenAIError,
45
45
  toOpenAIResponse: () => toOpenAIResponse,
46
- toOpenAIStreamChunk: () => toOpenAIStreamChunk
46
+ toOpenAIStreamChunk: () => toOpenAIStreamChunk,
47
+ toOpenAIUsageChunk: () => toOpenAIUsageChunk
47
48
  });
48
49
  module.exports = __toCommonJS(openai_format_exports);
49
50
  var import_crypto = __toESM(require("crypto"));
@@ -116,7 +117,19 @@ function toOpenAIStreamChunk(options) {
116
117
  logprobs: null,
117
118
  finish_reason: finishReason
118
119
  }
119
- ]
120
+ ],
121
+ usage: null
122
+ };
123
+ }
124
+ function toOpenAIUsageChunk(options) {
125
+ const { id, model, usage, object = "chat.completion.chunk" } = options;
126
+ return {
127
+ id,
128
+ object,
129
+ created: Math.floor(Date.now() / 1e3),
130
+ model,
131
+ choices: [],
132
+ usage
120
133
  };
121
134
  }
122
135
  function toOpenAIEmbeddingsResponse(options) {
@@ -154,5 +167,6 @@ function formatSSEDone() {
154
167
  toOpenAIEmbeddingsResponse,
155
168
  toOpenAIError,
156
169
  toOpenAIResponse,
157
- toOpenAIStreamChunk
170
+ toOpenAIStreamChunk,
171
+ toOpenAIUsageChunk
158
172
  });