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.
- package/README.md +51 -12
- package/dist/client/185.c47663fefaeb0e5b.js +10 -0
- package/dist/client/562.9012cfd1fa04303d.js +10 -0
- package/dist/client/685.b5b1e0a5b825d253.js +10 -0
- package/dist/client/index.js +1 -1
- package/dist/client-v2/185.b552dc91ec2371ba.js +10 -0
- package/dist/client-v2/562.db2984167250b1be.js +10 -0
- package/dist/client-v2/685.cf16e5b829e06f85.js +10 -0
- package/dist/client-v2/index.js +1 -1
- package/dist/externalVersion.js +8 -8
- package/dist/locale/en-US.json +175 -139
- package/dist/locale/vi-VN.json +40 -2
- package/dist/locale/zh-CN.json +40 -2
- package/dist/server/collections/ai-api-model-metadata.js +26 -0
- package/dist/server/collections/ai-api-response-records.js +101 -0
- package/dist/server/collections/ai-api-virtual-models.js +68 -0
- package/dist/server/middleware/response-record-resource.js +66 -0
- package/dist/server/middleware/role-permission.js +43 -18
- package/dist/server/migrations/20260901000000-remove-default-group-members.js +60 -0
- package/dist/server/migrations/20260902000000-seed-default-role-permissions.js +55 -0
- package/dist/server/migrations/20260903000000-seed-sample-response-records.js +170 -0
- package/dist/server/plugin.js +66 -16
- package/dist/server/routes/chat-completions.js +38 -6
- package/dist/server/routes/completions.js +16 -4
- package/dist/server/routes/embeddings.js +25 -6
- package/dist/server/routes/models.js +29 -0
- package/dist/server/routes/responses.js +530 -0
- package/dist/server/routes/router.js +65 -10
- package/dist/server/usage.js +25 -4
- package/dist/server/utils/direct-llm-context.js +1 -1
- package/dist/server/utils/resolve-service.js +24 -0
- package/dist/server/utils/response-store.js +138 -0
- package/dist/server/utils/responses-format.js +686 -0
- package/dist/server/utils/responses-stream.js +330 -0
- package/dist/server/utils/virtual-models.js +238 -0
- package/dist/server/validation.js +44 -2
- package/dist/swagger.js +137 -0
- package/package.json +34 -32
- package/src/__tests__/locale.test.ts +43 -0
- package/src/client/__tests__/settings-registration.test.tsx +1 -0
- package/src/client/plugin.tsx +9 -1
- package/src/client-v2/__tests__/settings-registration.test.tsx +1 -0
- package/src/client-v2/pages/ModelMetadataPage.tsx +44 -0
- package/src/client-v2/pages/ModelRoutingPage.tsx +238 -0
- package/src/client-v2/pages/UsageGroupsPage.tsx +75 -38
- package/src/client-v2/plugin.tsx +8 -0
- package/src/locale/en-US.json +175 -139
- package/src/locale/vi-VN.json +40 -2
- package/src/locale/zh-CN.json +40 -2
- package/src/server/__tests__/embeddings.test.ts +184 -0
- package/src/server/__tests__/models.test.ts +21 -1
- package/src/server/__tests__/response-record-resource.test.ts +50 -0
- package/src/server/__tests__/response-store-integration.test.ts +341 -0
- package/src/server/__tests__/response-store.test.ts +195 -0
- package/src/server/__tests__/responses-contract.test.ts +469 -0
- package/src/server/__tests__/responses-format.test.ts +299 -0
- package/src/server/__tests__/responses-router.test.ts +182 -0
- package/src/server/__tests__/responses-streaming.test.ts +368 -0
- package/src/server/__tests__/responses.test.ts +462 -0
- package/src/server/__tests__/role-permission.test.ts +139 -0
- package/src/server/__tests__/seed-role-permission.test.ts +88 -0
- package/src/server/__tests__/types/responses-sdk.types.test-d.ts +23 -0
- package/src/server/__tests__/usage-groups.test.ts +96 -0
- package/src/server/__tests__/usage-route.test.ts +1 -0
- package/src/server/__tests__/usage.test.ts +14 -0
- package/src/server/__tests__/validation.test.ts +66 -7
- package/src/server/__tests__/virtual-model-routing.test.ts +589 -0
- package/src/server/collections/ai-api-model-metadata.ts +26 -0
- package/src/server/collections/ai-api-response-records.ts +77 -0
- package/src/server/collections/ai-api-virtual-models.ts +58 -0
- package/src/server/middleware/response-record-resource.ts +44 -0
- package/src/server/middleware/role-permission.ts +69 -35
- package/src/server/migrations/20260901000000-remove-default-group-members.ts +56 -0
- package/src/server/migrations/20260902000000-seed-default-role-permissions.ts +46 -0
- package/src/server/migrations/20260903000000-seed-sample-response-records.ts +162 -0
- package/src/server/plugin.ts +84 -20
- package/src/server/resource/ai-api-config.ts +2 -1
- package/src/server/routes/agent-completions.ts +3 -0
- package/src/server/routes/chat-completions.ts +34 -10
- package/src/server/routes/completions.ts +16 -4
- package/src/server/routes/embeddings.ts +32 -10
- package/src/server/routes/models.ts +34 -0
- package/src/server/routes/responses.ts +640 -0
- package/src/server/routes/router.ts +81 -12
- package/src/server/services/__tests__/file-processor.test.ts +1 -0
- package/src/server/usage.ts +29 -2
- package/src/server/utils/app-observability.ts +1 -1
- package/src/server/utils/direct-llm-context.ts +2 -1
- package/src/server/utils/openai-format.ts +1 -0
- package/src/server/utils/resolve-service.ts +39 -1
- package/src/server/utils/response-store.ts +148 -0
- package/src/server/utils/responses-format.ts +974 -0
- package/src/server/utils/responses-stream.ts +384 -0
- package/src/server/utils/virtual-models.ts +320 -0
- package/src/server/validation.ts +49 -0
- package/src/swagger.ts +139 -0
- package/dist/client/562.44b16aad4718b4c7.js +0 -10
- package/dist/client/685.ae483e17b6b49c98.js +0 -10
- package/dist/client-v2/562.45d5c504433be38b.js +0 -10
- package/dist/client-v2/685.1030370b309b7d4b.js +0 -10
- package/dist/server/collections/ai-api-user-permissions.js +0 -67
- package/dist/server/collections/ai-api-user-quota-buckets.js +0 -54
- package/dist/server/collections/ai-api-user-quota-policies.js +0 -63
- package/dist/server/resource/ai-api-usage-groups.js +0 -168
- package/src/server/collections/ai-api-user-permissions.ts +0 -46
- package/src/server/collections/ai-api-user-quota-buckets.ts +0 -24
- package/src/server/collections/ai-api-user-quota-policies.ts +0 -33
- 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
|
});
|