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
package/dist/server/usage.js
CHANGED
|
@@ -60,6 +60,20 @@ function extractPromptCacheTokens(source) {
|
|
|
60
60
|
if (typeof source.cache_read === "number") return source.cache_read;
|
|
61
61
|
return null;
|
|
62
62
|
}
|
|
63
|
+
function extractReasoningTokens(source) {
|
|
64
|
+
const detailCandidates = [
|
|
65
|
+
source.output_token_details,
|
|
66
|
+
source.output_tokens_details,
|
|
67
|
+
source.completion_tokens_details
|
|
68
|
+
];
|
|
69
|
+
for (const details of detailCandidates) {
|
|
70
|
+
if (!details || typeof details !== "object") continue;
|
|
71
|
+
const record = details;
|
|
72
|
+
const value = normalizeTokenCount(record.reasoning ?? record.reasoning_tokens);
|
|
73
|
+
if (value !== null) return value;
|
|
74
|
+
}
|
|
75
|
+
return normalizeTokenCount(source.reasoning_tokens ?? source.reasoningTokens);
|
|
76
|
+
}
|
|
63
77
|
function normalizeUsage(value) {
|
|
64
78
|
if (!value || typeof value !== "object") return void 0;
|
|
65
79
|
const source = value;
|
|
@@ -77,7 +91,8 @@ function normalizeUsage(value) {
|
|
|
77
91
|
// Keep normalizeUsage idempotent: streaming routes normalize the chunk
|
|
78
92
|
// usage once and hand the result to setAiApiUsageResult, which normalizes
|
|
79
93
|
// again — an already-extracted prompt_cache_tokens must survive that pass.
|
|
80
|
-
prompt_cache_tokens: extractPromptCacheTokens(source) ?? normalizeTokenCount(source.prompt_cache_tokens)
|
|
94
|
+
prompt_cache_tokens: extractPromptCacheTokens(source) ?? normalizeTokenCount(source.prompt_cache_tokens),
|
|
95
|
+
reasoning_tokens: extractReasoningTokens(source) ?? normalizeTokenCount(source.reasoning_tokens)
|
|
81
96
|
};
|
|
82
97
|
}
|
|
83
98
|
function setAiApiUsageResult(ctx, value, metadata = {}, responseMetadata) {
|
|
@@ -126,7 +141,9 @@ async function startUsageRecord(ctx, requestId, endpoint, model, streaming, mode
|
|
|
126
141
|
values: {
|
|
127
142
|
requestId,
|
|
128
143
|
userId,
|
|
129
|
-
|
|
144
|
+
// When several roles granted access (union semantics), record all of them so the
|
|
145
|
+
// audit trail shows the full set, not just the first role in the list.
|
|
146
|
+
roleName: ((_b = state.currentRoles) == null ? void 0 : _b.length) ? state.currentRoles.join(",") : state.currentRole || "unknown",
|
|
130
147
|
authType: state.aiApiAuthType || (oauth ? "oidc" : "unknown"),
|
|
131
148
|
oauthClientId: oauth == null ? void 0 : oauth.clientId,
|
|
132
149
|
oauthSubject: oauth == null ? void 0 : oauth.subject,
|
|
@@ -141,7 +158,7 @@ async function startUsageRecord(ctx, requestId, endpoint, model, streaming, mode
|
|
|
141
158
|
messageCount: messages == null ? void 0 : messages.length,
|
|
142
159
|
promptCount,
|
|
143
160
|
embeddingInputCount,
|
|
144
|
-
requestedMaxTokens: body.max_completion_tokens ?? body.max_tokens
|
|
161
|
+
requestedMaxTokens: body.max_output_tokens ?? body.max_completion_tokens ?? body.max_tokens
|
|
145
162
|
}
|
|
146
163
|
}
|
|
147
164
|
});
|
|
@@ -184,7 +201,11 @@ async function finishUsageRecord(ctx, id, startedAt, status) {
|
|
|
184
201
|
durationMs: Date.now() - startedAt,
|
|
185
202
|
responseMetadata: {
|
|
186
203
|
usageSource: usageResult.source,
|
|
187
|
-
...gatewayResponseId ? { gatewayResponseId } : {}
|
|
204
|
+
...gatewayResponseId ? { gatewayResponseId } : {},
|
|
205
|
+
// When the request went through a virtual alias, keep both the alias and why
|
|
206
|
+
// it routed there; resolvedModel (from billing state) already holds the concrete model.
|
|
207
|
+
...state.aiApiVirtualModel ? { virtualModel: state.aiApiVirtualModel } : {},
|
|
208
|
+
...state.aiApiRoutingReason ? { routingReason: state.aiApiRoutingReason } : {}
|
|
188
209
|
}
|
|
189
210
|
};
|
|
190
211
|
await ctx.db.getRepository("aiApiUsageRecords").update({ filterByTk: id, values });
|
|
@@ -261,7 +261,7 @@ function resolveReservedOutputTokens(options, metadata) {
|
|
|
261
261
|
async function prepareDirectLlmContext(ctx, options) {
|
|
262
262
|
const [metadata, behavior] = await Promise.all([
|
|
263
263
|
loadModelMetadata(ctx, options.serviceName, options.modelId),
|
|
264
|
-
resolveOverflowBehavior(ctx)
|
|
264
|
+
options.overflowBehavior ? Promise.resolve(options.overflowBehavior) : resolveOverflowBehavior(ctx)
|
|
265
265
|
]);
|
|
266
266
|
const reservedOutputTokens = resolveReservedOutputTokens(options, metadata);
|
|
267
267
|
const inputTokenBudget = metadata.contextWindow - reservedOutputTokens;
|
|
@@ -27,6 +27,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
27
27
|
var resolve_service_exports = {};
|
|
28
28
|
__export(resolve_service_exports, {
|
|
29
29
|
resolveLlmService: () => resolveLlmService,
|
|
30
|
+
resolveModelReference: () => resolveModelReference,
|
|
30
31
|
resolveModelString: () => resolveModelString
|
|
31
32
|
});
|
|
32
33
|
module.exports = __toCommonJS(resolve_service_exports);
|
|
@@ -74,8 +75,31 @@ async function resolveModelString(ctx, modelString) {
|
|
|
74
75
|
}
|
|
75
76
|
return null;
|
|
76
77
|
}
|
|
78
|
+
async function resolveModelReference(ctx, reference) {
|
|
79
|
+
const repo = ctx.db.getRepository("llmServices");
|
|
80
|
+
const slashPositions = [];
|
|
81
|
+
for (let i = 0; i < reference.length; i++) {
|
|
82
|
+
if (reference[i] === "/") {
|
|
83
|
+
slashPositions.push(i);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
for (const pos of slashPositions) {
|
|
87
|
+
const serviceKey = reference.substring(0, pos);
|
|
88
|
+
const modelId = reference.substring(pos + 1);
|
|
89
|
+
if (!serviceKey || !modelId) continue;
|
|
90
|
+
let service = await repo.findOne({ filter: { name: serviceKey } });
|
|
91
|
+
if (!service) {
|
|
92
|
+
service = await repo.findOne({ filter: { title: serviceKey } });
|
|
93
|
+
}
|
|
94
|
+
if (service) {
|
|
95
|
+
return { service, modelId };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
77
100
|
// Annotate the CommonJS export names for ESM import in node:
|
|
78
101
|
0 && (module.exports = {
|
|
79
102
|
resolveLlmService,
|
|
103
|
+
resolveModelReference,
|
|
80
104
|
resolveModelString
|
|
81
105
|
});
|
|
@@ -0,0 +1,138 @@
|
|
|
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 response_store_exports = {};
|
|
28
|
+
__export(response_store_exports, {
|
|
29
|
+
RESPONSE_RETENTION_MS: () => RESPONSE_RETENTION_MS,
|
|
30
|
+
cleanupExpiredResponseRecords: () => cleanupExpiredResponseRecords,
|
|
31
|
+
deleteResponseRecord: () => deleteResponseRecord,
|
|
32
|
+
getResponseRecord: () => getResponseRecord,
|
|
33
|
+
loadConversationChain: () => loadConversationChain,
|
|
34
|
+
storeResponseRecord: () => storeResponseRecord
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(response_store_exports);
|
|
37
|
+
var import_responses_format = require("./responses-format");
|
|
38
|
+
const RETENTION_DAYS = 30;
|
|
39
|
+
const MS_PER_DAY = 24 * 60 * 60 * 1e3;
|
|
40
|
+
const RESPONSE_RETENTION_MS = RETENTION_DAYS * MS_PER_DAY;
|
|
41
|
+
function valueOf(model, key) {
|
|
42
|
+
if (!model) return void 0;
|
|
43
|
+
if (typeof model.get === "function") {
|
|
44
|
+
return model.get(key);
|
|
45
|
+
}
|
|
46
|
+
return model[key];
|
|
47
|
+
}
|
|
48
|
+
function isExpired(value) {
|
|
49
|
+
if (value === void 0 || value === null || value === "") return true;
|
|
50
|
+
const timestamp = value instanceof Date ? value.getTime() : new Date(String(value)).getTime();
|
|
51
|
+
return !Number.isFinite(timestamp) || timestamp <= Date.now();
|
|
52
|
+
}
|
|
53
|
+
async function storeResponseRecord(ctx, response, requestBody, userId) {
|
|
54
|
+
if (requestBody.store === false) return;
|
|
55
|
+
await ctx.db.getRepository("aiApiResponseRecords").create({
|
|
56
|
+
values: {
|
|
57
|
+
responseId: response.id,
|
|
58
|
+
userId,
|
|
59
|
+
model: response.model,
|
|
60
|
+
input: requestBody.input,
|
|
61
|
+
output: response,
|
|
62
|
+
previousResponseId: response.previous_response_id,
|
|
63
|
+
metadata: response.metadata,
|
|
64
|
+
expiresAt: new Date(Date.now() + RESPONSE_RETENTION_MS)
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
async function getResponseRecord(ctx, responseId, userId) {
|
|
69
|
+
const row = await ctx.db.getRepository("aiApiResponseRecords").findOne({
|
|
70
|
+
filter: { responseId, userId }
|
|
71
|
+
});
|
|
72
|
+
if (!row || isExpired(valueOf(row, "expiresAt"))) return null;
|
|
73
|
+
return {
|
|
74
|
+
id: valueOf(row, "id"),
|
|
75
|
+
responseId: valueOf(row, "responseId"),
|
|
76
|
+
userId: valueOf(row, "userId"),
|
|
77
|
+
model: valueOf(row, "model"),
|
|
78
|
+
input: valueOf(row, "input"),
|
|
79
|
+
output: valueOf(row, "output"),
|
|
80
|
+
previousResponseId: valueOf(row, "previousResponseId") ?? void 0,
|
|
81
|
+
metadata: valueOf(row, "metadata"),
|
|
82
|
+
expiresAt: valueOf(row, "expiresAt")
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function responseOutputToMessages(output) {
|
|
86
|
+
return (0, import_responses_format.responsesInputToMessages)(output.output);
|
|
87
|
+
}
|
|
88
|
+
async function loadConversationChain(ctx, responseId, userId, maxDepth = 100) {
|
|
89
|
+
const records = [];
|
|
90
|
+
const visited = /* @__PURE__ */ new Set();
|
|
91
|
+
let currentId = responseId;
|
|
92
|
+
while (currentId) {
|
|
93
|
+
if (records.length >= maxDepth || visited.has(currentId)) return null;
|
|
94
|
+
visited.add(currentId);
|
|
95
|
+
const record = await getResponseRecord(ctx, currentId, userId);
|
|
96
|
+
if (!record) return null;
|
|
97
|
+
records.unshift(record);
|
|
98
|
+
currentId = record.previousResponseId;
|
|
99
|
+
}
|
|
100
|
+
return records.flatMap((record) => [
|
|
101
|
+
...(0, import_responses_format.responsesInputToMessages)(record.input),
|
|
102
|
+
...responseOutputToMessages(record.output)
|
|
103
|
+
]);
|
|
104
|
+
}
|
|
105
|
+
async function cleanupExpiredResponseRecords(ctx) {
|
|
106
|
+
const repo = ctx.db.getRepository("aiApiResponseRecords");
|
|
107
|
+
const now = /* @__PURE__ */ new Date();
|
|
108
|
+
let deleted = 0;
|
|
109
|
+
for (; ; ) {
|
|
110
|
+
const expired = await repo.find({
|
|
111
|
+
filter: { expiresAt: { $lt: now } },
|
|
112
|
+
fields: ["id"],
|
|
113
|
+
limit: 1e3,
|
|
114
|
+
sort: "id"
|
|
115
|
+
});
|
|
116
|
+
const ids = expired.map((record) => record.get("id"));
|
|
117
|
+
if (ids.length === 0) return deleted;
|
|
118
|
+
const count = await repo.destroy({ filterByTk: ids, individualHooks: false });
|
|
119
|
+
if (typeof count === "number") deleted += count;
|
|
120
|
+
else deleted += ids.length;
|
|
121
|
+
if (count === 0) return deleted;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
async function deleteResponseRecord(ctx, responseId, userId) {
|
|
125
|
+
const record = await getResponseRecord(ctx, responseId, userId);
|
|
126
|
+
if (!record) return false;
|
|
127
|
+
await ctx.db.getRepository("aiApiResponseRecords").destroy({ filterByTk: record.id });
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
131
|
+
0 && (module.exports = {
|
|
132
|
+
RESPONSE_RETENTION_MS,
|
|
133
|
+
cleanupExpiredResponseRecords,
|
|
134
|
+
deleteResponseRecord,
|
|
135
|
+
getResponseRecord,
|
|
136
|
+
loadConversationChain,
|
|
137
|
+
storeResponseRecord
|
|
138
|
+
});
|