plugin-ai-api 1.1.1 → 1.1.2

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 (108) hide show
  1. package/README.md +51 -12
  2. package/dist/client/185.c47663fefaeb0e5b.js +10 -0
  3. package/dist/client/562.9012cfd1fa04303d.js +10 -0
  4. package/dist/client/685.b5b1e0a5b825d253.js +10 -0
  5. package/dist/client/index.js +1 -1
  6. package/dist/client-v2/185.b552dc91ec2371ba.js +10 -0
  7. package/dist/client-v2/562.db2984167250b1be.js +10 -0
  8. package/dist/client-v2/685.cf16e5b829e06f85.js +10 -0
  9. package/dist/client-v2/index.js +1 -1
  10. package/dist/externalVersion.js +8 -8
  11. package/dist/locale/en-US.json +175 -139
  12. package/dist/locale/vi-VN.json +40 -2
  13. package/dist/locale/zh-CN.json +40 -2
  14. package/dist/server/collections/ai-api-model-metadata.js +26 -0
  15. package/dist/server/collections/ai-api-response-records.js +101 -0
  16. package/dist/server/collections/ai-api-virtual-models.js +68 -0
  17. package/dist/server/middleware/response-record-resource.js +66 -0
  18. package/dist/server/middleware/role-permission.js +43 -18
  19. package/dist/server/migrations/20260901000000-remove-default-group-members.js +60 -0
  20. package/dist/server/migrations/20260902000000-seed-default-role-permissions.js +55 -0
  21. package/dist/server/migrations/20260903000000-seed-sample-response-records.js +170 -0
  22. package/dist/server/plugin.js +66 -16
  23. package/dist/server/routes/chat-completions.js +38 -6
  24. package/dist/server/routes/completions.js +16 -4
  25. package/dist/server/routes/embeddings.js +25 -6
  26. package/dist/server/routes/models.js +29 -0
  27. package/dist/server/routes/responses.js +530 -0
  28. package/dist/server/routes/router.js +65 -10
  29. package/dist/server/usage.js +25 -4
  30. package/dist/server/utils/direct-llm-context.js +1 -1
  31. package/dist/server/utils/resolve-service.js +24 -0
  32. package/dist/server/utils/response-store.js +138 -0
  33. package/dist/server/utils/responses-format.js +686 -0
  34. package/dist/server/utils/responses-stream.js +330 -0
  35. package/dist/server/utils/virtual-models.js +238 -0
  36. package/dist/server/validation.js +44 -2
  37. package/dist/swagger.js +137 -0
  38. package/package.json +34 -32
  39. package/src/__tests__/locale.test.ts +43 -0
  40. package/src/client/__tests__/settings-registration.test.tsx +1 -0
  41. package/src/client/plugin.tsx +9 -1
  42. package/src/client-v2/__tests__/settings-registration.test.tsx +1 -0
  43. package/src/client-v2/pages/ModelMetadataPage.tsx +44 -0
  44. package/src/client-v2/pages/ModelRoutingPage.tsx +238 -0
  45. package/src/client-v2/pages/UsageGroupsPage.tsx +75 -38
  46. package/src/client-v2/plugin.tsx +8 -0
  47. package/src/locale/en-US.json +175 -139
  48. package/src/locale/vi-VN.json +40 -2
  49. package/src/locale/zh-CN.json +40 -2
  50. package/src/server/__tests__/embeddings.test.ts +184 -0
  51. package/src/server/__tests__/models.test.ts +21 -1
  52. package/src/server/__tests__/response-record-resource.test.ts +50 -0
  53. package/src/server/__tests__/response-store-integration.test.ts +341 -0
  54. package/src/server/__tests__/response-store.test.ts +195 -0
  55. package/src/server/__tests__/responses-contract.test.ts +469 -0
  56. package/src/server/__tests__/responses-format.test.ts +299 -0
  57. package/src/server/__tests__/responses-router.test.ts +182 -0
  58. package/src/server/__tests__/responses-streaming.test.ts +368 -0
  59. package/src/server/__tests__/responses.test.ts +462 -0
  60. package/src/server/__tests__/role-permission.test.ts +139 -0
  61. package/src/server/__tests__/seed-role-permission.test.ts +88 -0
  62. package/src/server/__tests__/types/responses-sdk.types.test-d.ts +23 -0
  63. package/src/server/__tests__/usage-groups.test.ts +96 -0
  64. package/src/server/__tests__/usage-route.test.ts +1 -0
  65. package/src/server/__tests__/usage.test.ts +14 -0
  66. package/src/server/__tests__/validation.test.ts +66 -7
  67. package/src/server/__tests__/virtual-model-routing.test.ts +589 -0
  68. package/src/server/collections/ai-api-model-metadata.ts +26 -0
  69. package/src/server/collections/ai-api-response-records.ts +77 -0
  70. package/src/server/collections/ai-api-virtual-models.ts +58 -0
  71. package/src/server/middleware/response-record-resource.ts +44 -0
  72. package/src/server/middleware/role-permission.ts +69 -35
  73. package/src/server/migrations/20260901000000-remove-default-group-members.ts +56 -0
  74. package/src/server/migrations/20260902000000-seed-default-role-permissions.ts +46 -0
  75. package/src/server/migrations/20260903000000-seed-sample-response-records.ts +162 -0
  76. package/src/server/plugin.ts +84 -20
  77. package/src/server/resource/ai-api-config.ts +2 -1
  78. package/src/server/routes/agent-completions.ts +3 -0
  79. package/src/server/routes/chat-completions.ts +34 -10
  80. package/src/server/routes/completions.ts +16 -4
  81. package/src/server/routes/embeddings.ts +32 -10
  82. package/src/server/routes/models.ts +34 -0
  83. package/src/server/routes/responses.ts +640 -0
  84. package/src/server/routes/router.ts +81 -12
  85. package/src/server/services/__tests__/file-processor.test.ts +1 -0
  86. package/src/server/usage.ts +29 -2
  87. package/src/server/utils/app-observability.ts +1 -1
  88. package/src/server/utils/direct-llm-context.ts +2 -1
  89. package/src/server/utils/openai-format.ts +1 -0
  90. package/src/server/utils/resolve-service.ts +39 -1
  91. package/src/server/utils/response-store.ts +148 -0
  92. package/src/server/utils/responses-format.ts +974 -0
  93. package/src/server/utils/responses-stream.ts +384 -0
  94. package/src/server/utils/virtual-models.ts +320 -0
  95. package/src/server/validation.ts +49 -0
  96. package/src/swagger.ts +139 -0
  97. package/dist/client/562.44b16aad4718b4c7.js +0 -10
  98. package/dist/client/685.ae483e17b6b49c98.js +0 -10
  99. package/dist/client-v2/562.45d5c504433be38b.js +0 -10
  100. package/dist/client-v2/685.1030370b309b7d4b.js +0 -10
  101. package/dist/server/collections/ai-api-user-permissions.js +0 -67
  102. package/dist/server/collections/ai-api-user-quota-buckets.js +0 -54
  103. package/dist/server/collections/ai-api-user-quota-policies.js +0 -63
  104. package/dist/server/resource/ai-api-usage-groups.js +0 -168
  105. package/src/server/collections/ai-api-user-permissions.ts +0 -46
  106. package/src/server/collections/ai-api-user-quota-buckets.ts +0 -24
  107. package/src/server/collections/ai-api-user-quota-policies.ts +0 -33
  108. package/src/server/resource/ai-api-usage-groups.ts +0 -171
@@ -0,0 +1,330 @@
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 responses_stream_exports = {};
28
+ __export(responses_stream_exports, {
29
+ appendResponseStreamChunk: () => appendResponseStreamChunk,
30
+ createResponseErrorEvent: () => createResponseErrorEvent,
31
+ createResponseFailedEvent: () => createResponseFailedEvent,
32
+ createResponseStartEvents: () => createResponseStartEvents,
33
+ createResponseStreamState: () => createResponseStreamState,
34
+ finalizeResponseStream: () => finalizeResponseStream,
35
+ setResponseStreamUsage: () => setResponseStreamUsage
36
+ });
37
+ module.exports = __toCommonJS(responses_stream_exports);
38
+ var import_responses_format = require("./responses-format");
39
+ function event(state, value) {
40
+ return { ...value, sequence_number: state.sequenceNumber++ };
41
+ }
42
+ function createResponseStreamState(id, model, requestBody) {
43
+ return {
44
+ id,
45
+ model,
46
+ requestBody,
47
+ createdAt: Math.floor(Date.now() / 1e3),
48
+ sequenceNumber: 0,
49
+ nextOutputIndex: 0,
50
+ toolCalls: /* @__PURE__ */ new Map()
51
+ };
52
+ }
53
+ function responseSnapshot(state, status) {
54
+ var _a, _b;
55
+ const content = ((_a = state.textItem) == null ? void 0 : _a.text) ?? "";
56
+ const reasoningText = (_b = state.reasoningItem) == null ? void 0 : _b.text;
57
+ const toolCalls = [...state.toolCalls.values()].map(({ item }) => ({
58
+ id: item.call_id,
59
+ type: "function",
60
+ function: { name: item.name, arguments: item.arguments }
61
+ }));
62
+ const response = (0, import_responses_format.chatResultToResponse)({
63
+ id: state.id,
64
+ model: state.model,
65
+ content,
66
+ reasoningText,
67
+ usage: state.usage,
68
+ toolCalls,
69
+ finishReason: state.finishReason,
70
+ serviceTier: state.serviceTier,
71
+ requestBody: state.requestBody,
72
+ status
73
+ });
74
+ const indexedOutput = [];
75
+ if (state.reasoningItem) {
76
+ indexedOutput.push({
77
+ index: state.reasoningItem.outputIndex,
78
+ item: {
79
+ ...state.reasoningItem.item,
80
+ status: status === "in_progress" ? "in_progress" : status === "failed" ? "incomplete" : "completed"
81
+ }
82
+ });
83
+ }
84
+ if (state.textItem) {
85
+ indexedOutput.push({
86
+ index: state.textItem.outputIndex,
87
+ item: {
88
+ ...state.textItem.item,
89
+ status: status === "in_progress" ? "in_progress" : status === "failed" || (0, import_responses_format.responseIncompleteReason)(state.finishReason) ? "incomplete" : "completed",
90
+ content: state.textItem.text ? [{ type: "output_text", text: state.textItem.text, annotations: [] }] : []
91
+ }
92
+ });
93
+ }
94
+ for (const { item, outputIndex } of state.toolCalls.values()) {
95
+ indexedOutput.push({
96
+ index: outputIndex,
97
+ item: {
98
+ ...item,
99
+ status: status === "in_progress" ? "in_progress" : status === "failed" ? "incomplete" : "completed"
100
+ }
101
+ });
102
+ }
103
+ const output = indexedOutput.sort((left, right) => left.index - right.index).map(({ item }) => item);
104
+ response.created_at = state.createdAt;
105
+ response.output = output;
106
+ response.output_text = content;
107
+ response.usage = (0, import_responses_format.responseUsageFromOpenAIUsage)(state.usage);
108
+ return response;
109
+ }
110
+ function createResponseStartEvents(state) {
111
+ return [
112
+ event(state, { type: "response.created", response: responseSnapshot(state, "in_progress") }),
113
+ event(state, { type: "response.in_progress", response: responseSnapshot(state, "in_progress") })
114
+ ];
115
+ }
116
+ function ensureTextItem(state) {
117
+ if (state.textItem) return [];
118
+ const outputIndex = state.nextOutputIndex++;
119
+ const item = (0, import_responses_format.createResponseOutputMessage)("", "in_progress", (0, import_responses_format.generateResponseItemId)("msg"));
120
+ state.textItem = { outputIndex, item, text: "" };
121
+ const part = { type: "output_text", text: "", annotations: [] };
122
+ return [
123
+ event(state, { type: "response.output_item.added", output_index: outputIndex, item }),
124
+ event(state, {
125
+ type: "response.content_part.added",
126
+ item_id: item.id,
127
+ output_index: outputIndex,
128
+ content_index: 0,
129
+ part
130
+ })
131
+ ];
132
+ }
133
+ function ensureReasoningItem(state) {
134
+ if (state.reasoningItem) return [];
135
+ const outputIndex = state.nextOutputIndex++;
136
+ const item = (0, import_responses_format.createResponseReasoningItem)("");
137
+ item.status = "in_progress";
138
+ state.reasoningItem = { outputIndex, item, text: "" };
139
+ return [
140
+ event(state, { type: "response.output_item.added", output_index: outputIndex, item }),
141
+ event(state, {
142
+ type: "response.content_part.added",
143
+ item_id: item.id,
144
+ output_index: outputIndex,
145
+ content_index: 0,
146
+ part: { type: "reasoning_text", text: "" }
147
+ })
148
+ ];
149
+ }
150
+ function isRecord(value) {
151
+ return typeof value === "object" && value !== null && !Array.isArray(value);
152
+ }
153
+ function appendToolCallChunks(state, value) {
154
+ if (!Array.isArray(value)) return [];
155
+ const events = [];
156
+ for (const [fallbackIndex, raw] of value.entries()) {
157
+ if (!isRecord(raw)) continue;
158
+ const index = typeof raw.index === "number" ? raw.index : fallbackIndex;
159
+ const functionValue = isRecord(raw.function) ? raw.function : void 0;
160
+ const id = typeof raw.id === "string" ? raw.id : void 0;
161
+ const name = typeof raw.name === "string" ? raw.name : typeof (functionValue == null ? void 0 : functionValue.name) === "string" ? functionValue.name : void 0;
162
+ const argumentsDelta = typeof raw.args === "string" ? raw.args : typeof (functionValue == null ? void 0 : functionValue.arguments) === "string" ? functionValue.arguments : raw.args === void 0 ? "" : JSON.stringify(raw.args);
163
+ let call = state.toolCalls.get(index);
164
+ if (!call) {
165
+ const callId = id ?? (0, import_responses_format.generateResponseItemId)("fc");
166
+ const item = {
167
+ id: (0, import_responses_format.generateResponseItemId)("fc"),
168
+ type: "function_call",
169
+ status: "in_progress",
170
+ call_id: callId,
171
+ name: name ?? "",
172
+ arguments: ""
173
+ };
174
+ call = { outputIndex: state.nextOutputIndex++, item };
175
+ state.toolCalls.set(index, call);
176
+ events.push(event(state, { type: "response.output_item.added", output_index: call.outputIndex, item }));
177
+ }
178
+ if (name) call.item.name = name;
179
+ if (id) call.item.call_id = id;
180
+ if (argumentsDelta) {
181
+ call.item.arguments += argumentsDelta;
182
+ events.push(
183
+ event(state, {
184
+ type: "response.function_call_arguments.delta",
185
+ item_id: call.item.id,
186
+ output_index: call.outputIndex,
187
+ delta: argumentsDelta
188
+ })
189
+ );
190
+ }
191
+ }
192
+ return events;
193
+ }
194
+ function appendResponseStreamChunk(state, chunk) {
195
+ if (!isRecord(chunk)) return [];
196
+ const events = [];
197
+ const reasoningDelta = (0, import_responses_format.extractReasoningText)(chunk);
198
+ if (reasoningDelta) {
199
+ events.push(...ensureReasoningItem(state));
200
+ const reasoning = state.reasoningItem;
201
+ reasoning.text += reasoningDelta;
202
+ reasoning.item.content = [{ type: "reasoning_text", text: reasoning.text }];
203
+ events.push(
204
+ event(state, {
205
+ type: "response.reasoning_text.delta",
206
+ item_id: reasoning.item.id,
207
+ output_index: reasoning.outputIndex,
208
+ content_index: 0,
209
+ delta: reasoningDelta
210
+ })
211
+ );
212
+ }
213
+ const content = (0, import_responses_format.extractResponseOutputText)(chunk);
214
+ if (content) {
215
+ events.push(...ensureTextItem(state));
216
+ const text = state.textItem;
217
+ text.text += content;
218
+ text.item.content = [{ type: "output_text", text: text.text, annotations: [] }];
219
+ events.push(
220
+ event(state, {
221
+ type: "response.output_text.delta",
222
+ item_id: text.item.id,
223
+ output_index: text.outputIndex,
224
+ content_index: 0,
225
+ delta: content,
226
+ logprobs: []
227
+ })
228
+ );
229
+ }
230
+ events.push(...appendToolCallChunks(state, chunk.tool_call_chunks));
231
+ const finishReason = [chunk.response_metadata, chunk.additional_kwargs].filter(isRecord).map((metadata) => metadata.finish_reason).find((candidate) => typeof candidate === "string" && candidate.length > 0);
232
+ const incompleteReason = (0, import_responses_format.extractResponseIncompleteReason)(chunk);
233
+ if (finishReason || incompleteReason) state.finishReason = finishReason ?? incompleteReason;
234
+ state.serviceTier = state.serviceTier ?? (0, import_responses_format.extractResponseServiceTier)(chunk);
235
+ return events;
236
+ }
237
+ function setResponseStreamUsage(state, usage) {
238
+ if (usage) state.usage = usage;
239
+ }
240
+ function finalizeResponseStream(state) {
241
+ const events = [];
242
+ const incompleteReason = (0, import_responses_format.responseIncompleteReason)(state.finishReason);
243
+ if (state.reasoningItem) {
244
+ const { item, outputIndex, text } = state.reasoningItem;
245
+ item.status = "completed";
246
+ events.push(
247
+ event(state, {
248
+ type: "response.reasoning_text.done",
249
+ item_id: item.id,
250
+ output_index: outputIndex,
251
+ content_index: 0,
252
+ text
253
+ }),
254
+ event(state, {
255
+ type: "response.content_part.done",
256
+ item_id: item.id,
257
+ output_index: outputIndex,
258
+ content_index: 0,
259
+ part: { type: "reasoning_text", text }
260
+ }),
261
+ event(state, { type: "response.output_item.done", output_index: outputIndex, item })
262
+ );
263
+ }
264
+ if (state.textItem) {
265
+ const { item, outputIndex, text } = state.textItem;
266
+ item.status = incompleteReason ? "incomplete" : "completed";
267
+ const part = { type: "output_text", text, annotations: [] };
268
+ item.content = [part];
269
+ events.push(
270
+ event(state, {
271
+ type: "response.output_text.done",
272
+ item_id: item.id,
273
+ output_index: outputIndex,
274
+ content_index: 0,
275
+ text,
276
+ logprobs: []
277
+ }),
278
+ event(state, {
279
+ type: "response.content_part.done",
280
+ item_id: item.id,
281
+ output_index: outputIndex,
282
+ content_index: 0,
283
+ part
284
+ }),
285
+ event(state, { type: "response.output_item.done", output_index: outputIndex, item })
286
+ );
287
+ }
288
+ for (const { item, outputIndex } of state.toolCalls.values()) {
289
+ item.status = "completed";
290
+ events.push(
291
+ event(state, {
292
+ type: "response.function_call_arguments.done",
293
+ item_id: item.id,
294
+ output_index: outputIndex,
295
+ name: item.name,
296
+ arguments: item.arguments
297
+ }),
298
+ event(state, { type: "response.output_item.done", output_index: outputIndex, item })
299
+ );
300
+ }
301
+ const response = responseSnapshot(state, incompleteReason ? "incomplete" : "completed");
302
+ events.push(
303
+ event(state, { type: response.status === "incomplete" ? "response.incomplete" : "response.completed", response })
304
+ );
305
+ return events;
306
+ }
307
+ function createResponseErrorEvent(state, error) {
308
+ return event(state, {
309
+ type: "error",
310
+ code: error.code ?? "server_error",
311
+ message: error.message,
312
+ param: null
313
+ });
314
+ }
315
+ function createResponseFailedEvent(state, error) {
316
+ const response = responseSnapshot(state, "failed");
317
+ response.error = { code: error.code ?? "server_error", message: error.message };
318
+ response.completed_at = null;
319
+ return event(state, { type: "response.failed", response });
320
+ }
321
+ // Annotate the CommonJS export names for ESM import in node:
322
+ 0 && (module.exports = {
323
+ appendResponseStreamChunk,
324
+ createResponseErrorEvent,
325
+ createResponseFailedEvent,
326
+ createResponseStartEvents,
327
+ createResponseStreamState,
328
+ finalizeResponseStream,
329
+ setResponseStreamUsage
330
+ });
@@ -0,0 +1,238 @@
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 virtual_models_exports = {};
28
+ __export(virtual_models_exports, {
29
+ detectRequestSignals: () => detectRequestSignals,
30
+ listAccessibleVirtualModels: () => listAccessibleVirtualModels,
31
+ resolveVirtualModel: () => resolveVirtualModel,
32
+ respondVirtualModelUnavailable: () => respondVirtualModelUnavailable
33
+ });
34
+ module.exports = __toCommonJS(virtual_models_exports);
35
+ var import_openai_format = require("./openai-format");
36
+ var import_resolve_service = require("./resolve-service");
37
+ var import_user_permissions = require("./user-permissions");
38
+ var import_request_cache = require("./request-cache");
39
+ function isRecord(value) {
40
+ return typeof value === "object" && value !== null;
41
+ }
42
+ function contentHasImage(content) {
43
+ if (!Array.isArray(content)) return false;
44
+ return content.some((block) => {
45
+ if (!isRecord(block)) return false;
46
+ if (block.type === "image_url") return true;
47
+ if (block.type === "file") return true;
48
+ if (block.type === "file_url") return true;
49
+ return false;
50
+ });
51
+ }
52
+ function detectRequestSignals(body) {
53
+ const messages = Array.isArray(body.messages) ? body.messages : [];
54
+ const hasImage = messages.some((msg) => isRecord(msg) && contentHasImage(msg.content));
55
+ const hasTools = Array.isArray(body.tools) && body.tools.length > 0;
56
+ const rf = body.response_format;
57
+ const wantsStructuredOutput = isRecord(rf) && (rf.type === "json_object" || rf.type === "json_schema");
58
+ const wantsReasoning = Object.hasOwn(body, "reasoning") && body.reasoning !== void 0 && body.reasoning !== null || Object.hasOwn(body, "reasoning_effort") && body.reasoning_effort !== void 0 && body.reasoning_effort !== null;
59
+ return { hasImage, hasTools, wantsStructuredOutput, wantsReasoning };
60
+ }
61
+ function valueOf(model, key) {
62
+ if (!model) return void 0;
63
+ if (typeof model.get === "function") {
64
+ return model.get(key);
65
+ }
66
+ return model[key];
67
+ }
68
+ function stringList(value) {
69
+ return Array.isArray(value) ? value.filter((v) => typeof v === "string" && v.length > 0) : [];
70
+ }
71
+ function toVirtualModel(row, fallbackName = "") {
72
+ return {
73
+ name: String(valueOf(row, "name") ?? fallbackName),
74
+ mode: valueOf(row, "mode") === "embedding" ? "embedding" : "chat",
75
+ fallbackModel: String(valueOf(row, "fallbackModel") ?? ""),
76
+ visionModels: stringList(valueOf(row, "visionModels")),
77
+ toolModels: stringList(valueOf(row, "toolModels")),
78
+ reasoningModels: stringList(valueOf(row, "reasoningModels")),
79
+ cheapModels: stringList(valueOf(row, "cheapModels")),
80
+ generalModels: stringList(valueOf(row, "generalModels")),
81
+ enabled: valueOf(row, "enabled")
82
+ };
83
+ }
84
+ async function loadVirtualModel(ctx, name) {
85
+ const row = await ctx.db.getRepository("aiApiVirtualModels").findOne({ filter: { name, enabled: true } });
86
+ return row ? toVirtualModel(row, name) : null;
87
+ }
88
+ async function deriveBucket(ctx, capability) {
89
+ const repo = ctx.db.getRepository("aiApiModelMetadata");
90
+ const filter = { enabled: true };
91
+ if (capability === "vision") filter.supportsVision = true;
92
+ else if (capability === "tool") filter.supportsToolCalling = true;
93
+ else if (capability === "reasoning") filter.reasoningTier = "reasoning";
94
+ else filter.reasoningTier = { $in: ["general", "cheap", "reasoning"] };
95
+ const rows = await repo.find({ filter, sort: "sortOrder", pageSize: 200 });
96
+ const list = rows.map((row) => {
97
+ const service = valueOf(row, "llmService");
98
+ const model = valueOf(row, "model");
99
+ return service && model ? `${service}/${model}` : "";
100
+ });
101
+ return list.filter(Boolean);
102
+ }
103
+ async function bucketFor(ctx, vm, signals) {
104
+ const explicit = (list) => list && list.length ? list : null;
105
+ if (signals.hasImage) {
106
+ return { reason: "vision", candidates: explicit(vm.visionModels) ?? await deriveBucket(ctx, "vision") };
107
+ }
108
+ if (signals.hasTools) {
109
+ return { reason: "tools", candidates: explicit(vm.toolModels) ?? await deriveBucket(ctx, "tool") };
110
+ }
111
+ if (signals.wantsReasoning) {
112
+ return { reason: "reasoning", candidates: explicit(vm.reasoningModels) ?? await deriveBucket(ctx, "reasoning") };
113
+ }
114
+ if (signals.wantsStructuredOutput) {
115
+ return {
116
+ reason: "structured_output",
117
+ candidates: explicit(vm.generalModels) ?? await deriveBucket(ctx, "general")
118
+ };
119
+ }
120
+ const cheap = explicit(vm.cheapModels);
121
+ if (cheap) return { reason: "cheap", candidates: cheap };
122
+ return { reason: "general", candidates: explicit(vm.generalModels) ?? await deriveBucket(ctx, "general") };
123
+ }
124
+ async function resolveVirtualModel(ctx, alias, body, requestedMode) {
125
+ const vm = await loadVirtualModel(ctx, alias);
126
+ if (!vm) return null;
127
+ if (vm.mode !== requestedMode) {
128
+ return {
129
+ status: "unavailable",
130
+ virtualModel: alias,
131
+ reason: "mode_mismatch",
132
+ configuredMode: vm.mode,
133
+ requestedMode
134
+ };
135
+ }
136
+ const config = await (0, import_request_cache.getAiApiConfig)(ctx);
137
+ const globalServices = valueOf(config, "enabledLlmServices") ?? [];
138
+ const scope = await (0, import_user_permissions.resolveUserAccessScope)(ctx);
139
+ if (scope.lookupFailed) {
140
+ return {
141
+ status: "unavailable",
142
+ virtualModel: alias,
143
+ reason: "permission_check_failed",
144
+ configuredMode: vm.mode,
145
+ requestedMode
146
+ };
147
+ }
148
+ const usable = (service, modelId) => valueOf(service, "enabled") !== false && (0, import_user_permissions.isServiceAllowed)(scope, globalServices, {
149
+ name: valueOf(service, "name"),
150
+ title: valueOf(service, "title")
151
+ }) && (0, import_user_permissions.isModelAllowed)(scope, `${valueOf(service, "name")}/${modelId}`);
152
+ if (requestedMode === "embedding") {
153
+ const fallback2 = await (0, import_resolve_service.resolveModelReference)(ctx, vm.fallbackModel);
154
+ if (fallback2 && usable(fallback2.service, fallback2.modelId)) {
155
+ return { status: "resolved", virtualModel: alias, reason: "fallback", resolved: fallback2 };
156
+ }
157
+ return {
158
+ status: "unavailable",
159
+ virtualModel: alias,
160
+ reason: "no_permitted_model",
161
+ configuredMode: vm.mode,
162
+ requestedMode
163
+ };
164
+ }
165
+ const signals = detectRequestSignals(body);
166
+ const { reason, candidates } = await bucketFor(ctx, vm, signals);
167
+ for (const candidate of candidates) {
168
+ const resolved = await (0, import_resolve_service.resolveModelReference)(ctx, candidate);
169
+ if (resolved && usable(resolved.service, resolved.modelId)) {
170
+ return { status: "resolved", virtualModel: alias, reason, resolved };
171
+ }
172
+ }
173
+ const fallback = await (0, import_resolve_service.resolveModelReference)(ctx, vm.fallbackModel);
174
+ if (fallback && usable(fallback.service, fallback.modelId)) {
175
+ return { status: "resolved", virtualModel: alias, reason: "fallback", resolved: fallback };
176
+ }
177
+ return {
178
+ status: "unavailable",
179
+ virtualModel: alias,
180
+ reason: "no_permitted_model",
181
+ configuredMode: vm.mode,
182
+ requestedMode
183
+ };
184
+ }
185
+ async function listAccessibleVirtualModels(ctx, scope, globalServices) {
186
+ const rows = await ctx.db.getRepository("aiApiVirtualModels").find({ filter: { enabled: true } });
187
+ const accessible = [];
188
+ for (const row of rows) {
189
+ const vm = toVirtualModel(row);
190
+ if (!vm.fallbackModel) continue;
191
+ const resolved = await (0, import_resolve_service.resolveModelReference)(ctx, vm.fallbackModel);
192
+ if (!resolved || valueOf(resolved.service, "enabled") === false) continue;
193
+ const service = {
194
+ name: valueOf(resolved.service, "name"),
195
+ title: valueOf(resolved.service, "title")
196
+ };
197
+ if ((0, import_user_permissions.isServiceAllowed)(scope, globalServices, service) && (0, import_user_permissions.isModelAllowed)(scope, `${service.name}/${resolved.modelId}`)) {
198
+ accessible.push(vm);
199
+ }
200
+ }
201
+ return accessible;
202
+ }
203
+ function respondVirtualModelUnavailable(ctx, result) {
204
+ if (result.reason === "permission_check_failed") {
205
+ ctx.status = 503;
206
+ ctx.body = (0, import_openai_format.toOpenAIError)(
207
+ 503,
208
+ "Unable to verify LLM permissions for this user. Please retry shortly.",
209
+ "service_unavailable",
210
+ "permission_check_failed"
211
+ );
212
+ return;
213
+ }
214
+ if (result.reason === "mode_mismatch") {
215
+ ctx.status = 404;
216
+ ctx.body = (0, import_openai_format.toOpenAIError)(
217
+ 404,
218
+ `Virtual model '${result.virtualModel}' serves ${result.configuredMode} requests and cannot be used for ${result.requestedMode} requests.`,
219
+ "invalid_request_error",
220
+ "model_not_found"
221
+ );
222
+ return;
223
+ }
224
+ ctx.status = 403;
225
+ ctx.body = (0, import_openai_format.toOpenAIError)(
226
+ 403,
227
+ `No model behind virtual alias '${result.virtualModel}' is available to this user. Use GET /v1/models to see available models.`,
228
+ "permission_denied",
229
+ "model_not_available"
230
+ );
231
+ }
232
+ // Annotate the CommonJS export names for ESM import in node:
233
+ 0 && (module.exports = {
234
+ detectRequestSignals,
235
+ listAccessibleVirtualModels,
236
+ resolveVirtualModel,
237
+ respondVirtualModelUnavailable
238
+ });
@@ -38,7 +38,8 @@ var validation_exports = {};
38
38
  __export(validation_exports, {
39
39
  validateModelMetadata: () => validateModelMetadata,
40
40
  validateModelPrice: () => validateModelPrice,
41
- validateQuotaPolicy: () => validateQuotaPolicy
41
+ validateQuotaPolicy: () => validateQuotaPolicy,
42
+ validateVirtualModel: () => validateVirtualModel
42
43
  });
43
44
  module.exports = __toCommonJS(validation_exports);
44
45
  var import_dayjs = __toESM(require("dayjs"));
@@ -84,6 +85,14 @@ function requirePositiveIntegerOrNull(value, field) {
84
85
  const parsed = Number(value);
85
86
  if (!Number.isSafeInteger(parsed) || parsed <= 0) throw new Error(`${field} must be a positive integer.`);
86
87
  }
88
+ function requireBooleanOrNull(value, field) {
89
+ if (value === null || value === void 0) return;
90
+ if (typeof value !== "boolean") throw new Error(`${field} must be a boolean.`);
91
+ }
92
+ function requireInteger(value, field) {
93
+ if (value === null || value === void 0 || value === "") return;
94
+ if (!Number.isSafeInteger(Number(value))) throw new Error(`${field} must be an integer.`);
95
+ }
87
96
  function validateModelMetadata(model) {
88
97
  if (!String(model.get("llmService") ?? "").trim()) throw new Error("llmService is required.");
89
98
  if (!String(model.get("model") ?? "").trim()) throw new Error("model is required.");
@@ -93,12 +102,44 @@ function validateModelMetadata(model) {
93
102
  if (systemPrompt !== null && systemPrompt !== void 0 && typeof systemPrompt !== "string") {
94
103
  throw new Error("systemPrompt must be a string.");
95
104
  }
105
+ requireBooleanOrNull(model.get("supportsVision"), "supportsVision");
106
+ requireBooleanOrNull(model.get("supportsToolCalling"), "supportsToolCalling");
107
+ requireBooleanOrNull(model.get("enabled"), "enabled");
108
+ const reasoningTier = model.get("reasoningTier");
109
+ if (reasoningTier !== null && reasoningTier !== void 0 && !["cheap", "general", "reasoning"].includes(String(reasoningTier))) {
110
+ throw new Error("reasoningTier must be cheap, general, or reasoning.");
111
+ }
112
+ requireInteger(model.get("sortOrder"), "sortOrder");
96
113
  const contextWindow = model.get("contextWindow");
97
114
  const maxCompletionTokens = model.get("maxCompletionTokens");
98
115
  if (contextWindow !== null && contextWindow !== void 0 && contextWindow !== "" && maxCompletionTokens !== null && maxCompletionTokens !== void 0 && maxCompletionTokens !== "" && Number(maxCompletionTokens) > Number(contextWindow)) {
99
116
  throw new Error("maxCompletionTokens cannot exceed contextWindow.");
100
117
  }
101
118
  }
119
+ function requireModelReference(value, field) {
120
+ const normalized = typeof value === "string" ? value.trim() : "";
121
+ const slash = normalized.indexOf("/");
122
+ if (slash <= 0 || slash === normalized.length - 1) {
123
+ throw new Error(`${field} must use the service/modelId format.`);
124
+ }
125
+ }
126
+ function requireModelReferenceList(value, field) {
127
+ if (value === null || value === void 0) return;
128
+ if (!Array.isArray(value)) throw new Error(`${field} must be an array.`);
129
+ value.forEach((item, index) => requireModelReference(item, `${field}[${index}]`));
130
+ }
131
+ function validateVirtualModel(model) {
132
+ if (!String(model.get("name") ?? "").trim()) throw new Error("name is required.");
133
+ const mode = model.get("mode");
134
+ if (mode !== null && mode !== void 0 && !["chat", "embedding"].includes(String(mode))) {
135
+ throw new Error("mode must be chat or embedding.");
136
+ }
137
+ requireModelReference(model.get("fallbackModel"), "fallbackModel");
138
+ for (const field of ["visionModels", "toolModels", "reasoningModels", "cheapModels", "generalModels"]) {
139
+ requireModelReferenceList(model.get(field), field);
140
+ }
141
+ requireBooleanOrNull(model.get("enabled"), "enabled");
142
+ }
102
143
  function validateQuotaPolicy(model) {
103
144
  if (!["daily", "monthly"].includes(String(model.get("periodType")))) {
104
145
  throw new Error("periodType must be daily or monthly.");
@@ -127,5 +168,6 @@ function validateQuotaPolicy(model) {
127
168
  0 && (module.exports = {
128
169
  validateModelMetadata,
129
170
  validateModelPrice,
130
- validateQuotaPolicy
171
+ validateQuotaPolicy,
172
+ validateVirtualModel
131
173
  });