plugin-ai-api 1.0.24 → 1.0.28
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/dist/client/{286.01c0e3c5fff3cccb.js → 286.a1ee0420172cd5de.js} +1 -1
- package/dist/client/302.fbc46ebf5bf300d7.js +10 -0
- package/dist/client/562.44b16aad4718b4c7.js +10 -0
- package/dist/client/685.ae483e17b6b49c98.js +10 -0
- package/dist/client/757.6568d3504ad29352.js +10 -0
- package/dist/client/{97.72979a11a067a7c9.js → 97.9b6b2d2b01a4c060.js} +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client-v2/302.3971233415999b2c.js +10 -0
- package/dist/client-v2/562.45d5c504433be38b.js +10 -0
- package/dist/client-v2/685.1030370b309b7d4b.js +10 -0
- package/dist/client-v2/757.f2bc9cfba07004b0.js +10 -0
- package/dist/client-v2/{952.94100128b7757f56.js → 952.f0249eddc153bde1.js} +1 -1
- package/dist/client-v2/{97.29c663318eebbd57.js → 97.36a42eff36bb3d8a.js} +1 -1
- package/dist/client-v2/index.js +1 -1
- package/dist/constants.js +2 -5
- package/dist/externalVersion.js +8 -8
- package/dist/locale/en-US.json +27 -8
- package/dist/locale/vi-VN.json +27 -8
- package/dist/locale/zh-CN.json +27 -8
- package/dist/server/billing.js +31 -33
- package/dist/server/collections/ai-api-config.js +7 -7
- package/dist/server/collections/ai-api-group-members.js +62 -0
- package/dist/server/collections/ai-api-group-quota-buckets.js +63 -0
- package/dist/server/collections/ai-api-model-metadata.js +6 -0
- package/dist/server/collections/ai-api-usage-groups.js +74 -0
- package/dist/server/collections/ai-api-usage-records.js +2 -0
- package/dist/server/middleware/rate-limit.js +7 -6
- package/dist/server/migrations/20260813000000-add-prompt-cache-tokens.js +69 -0
- package/dist/server/migrations/20260815000000-add-usage-groups.js +149 -0
- package/dist/server/migrations/20260816000000-migrate-user-permissions-to-groups.js +169 -0
- package/dist/server/migrations/20260816100000-add-model-metadata-system-prompt.js +69 -0
- package/dist/server/plugin.js +100 -22
- package/dist/server/quota-groups.js +108 -0
- package/dist/server/resource/ai-api-config.js +5 -3
- package/dist/server/resource/ai-api-usage-groups.js +168 -0
- package/dist/server/resource/ai-api-usage-monitor.js +3 -1
- package/dist/server/routes/agent-completions.js +2 -1
- package/dist/server/routes/chat-completions.js +121 -42
- package/dist/server/routes/completions.js +48 -29
- package/dist/server/routes/embeddings.js +2 -1
- package/dist/server/routes/models.js +2 -1
- package/dist/server/routes/router.js +3 -2
- package/dist/server/services/file-processor.js +426 -0
- package/dist/server/usage.js +37 -3
- package/dist/server/utils/direct-llm-context.js +163 -26
- package/dist/server/utils/openai-format.js +21 -2
- package/dist/server/utils/rate-limiter.js +1 -1
- package/dist/server/utils/request-cache.js +61 -0
- package/dist/server/utils/resolve-service.js +2 -1
- package/dist/server/utils/user-permissions.js +25 -39
- package/dist/server/validation.js +7 -0
- package/dist/swagger.js +48 -10
- package/package.json +1 -1
- package/src/client/__tests__/settings-registration.test.tsx +6 -29
- package/src/client/plugin.tsx +5 -16
- package/src/client-v2/__tests__/settings-registration.test.tsx +6 -32
- package/src/client-v2/locale.ts +3 -1
- package/src/client-v2/pages/GeneralPage.tsx +0 -5
- package/src/client-v2/pages/ModelMetadataPage.tsx +20 -1
- package/src/client-v2/pages/UsageGroupsPage.tsx +548 -0
- package/src/client-v2/pages/UsagePage.tsx +9 -0
- package/src/client-v2/plugin.tsx +4 -13
- package/src/constants.ts +0 -7
- package/src/locale/en-US.json +27 -8
- package/src/locale/vi-VN.json +27 -8
- package/src/locale/zh-CN.json +27 -8
- package/src/server/__tests__/billing-quota.test.ts +28 -9
- package/src/server/__tests__/direct-llm-context.test.ts +209 -10
- package/src/server/__tests__/file-processor.test.ts +225 -0
- package/src/server/__tests__/models.test.ts +1 -1
- package/src/server/__tests__/openai-format.test.ts +12 -2
- package/src/server/__tests__/permission-sync.test.ts +34 -35
- package/src/server/__tests__/request-body.test.ts +45 -2
- package/src/server/__tests__/usage-groups.test.ts +160 -0
- package/src/server/__tests__/usage-monitor.test.ts +2 -0
- package/src/server/__tests__/usage-route.test.ts +382 -5
- package/src/server/__tests__/usage.test.ts +57 -0
- package/src/server/__tests__/user-permissions.test.ts +214 -133
- package/src/server/__tests__/validation.test.ts +11 -0
- package/src/server/billing.ts +36 -39
- package/src/server/collections/ai-api-config.ts +9 -7
- package/src/server/collections/ai-api-group-members.ts +41 -0
- package/src/server/collections/ai-api-group-quota-buckets.ts +42 -0
- package/src/server/collections/ai-api-model-metadata.ts +7 -0
- package/src/server/collections/ai-api-role-permissions.ts +41 -41
- package/src/server/collections/ai-api-usage-groups.ts +53 -0
- package/src/server/collections/ai-api-usage-records.ts +2 -0
- package/src/server/index.ts +10 -10
- package/src/server/middleware/rate-limit.ts +68 -70
- package/src/server/migrations/20260813000000-add-prompt-cache-tokens.ts +46 -0
- package/src/server/migrations/20260815000000-add-usage-groups.ts +147 -0
- package/src/server/migrations/20260816000000-migrate-user-permissions-to-groups.ts +190 -0
- package/src/server/migrations/20260816100000-add-model-metadata-system-prompt.ts +46 -0
- package/src/server/plugin.ts +121 -30
- package/src/server/quota-groups.ts +117 -0
- package/src/server/resource/ai-api-config.ts +5 -3
- package/src/server/resource/ai-api-usage-groups.ts +171 -0
- package/src/server/resource/ai-api-usage-monitor.ts +3 -0
- package/src/server/routes/agent-completions.ts +2 -1
- package/src/server/routes/chat-completions.ts +173 -47
- package/src/server/routes/completions.ts +50 -27
- package/src/server/routes/embeddings.ts +2 -1
- package/src/server/routes/models.ts +4 -3
- package/src/server/routes/router.ts +4 -3
- package/src/server/services/__tests__/file-processor.test.ts +184 -0
- package/src/server/services/file-processor.ts +513 -0
- package/src/server/usage.ts +51 -1
- package/src/server/utils/direct-llm-context.ts +218 -31
- package/src/server/utils/openai-format.ts +25 -2
- package/src/server/utils/rate-limiter.ts +83 -83
- package/src/server/utils/request-cache.ts +59 -0
- package/src/server/utils/resolve-service.ts +83 -82
- package/src/server/utils/user-permissions.ts +49 -69
- package/src/server/validation.ts +7 -0
- package/src/swagger.ts +52 -11
- package/dist/client/123.e6fe04c856ce6417.js +0 -10
- package/dist/client/302.fc3a3491b4ec2dfd.js +0 -10
- package/dist/client/562.17a0a299d2e5152c.js +0 -10
- package/dist/client/757.a01403fb7a1bea01.js +0 -10
- package/dist/client/902.e74518750f1e4201.js +0 -10
- package/dist/client-v2/123.05f1f649923f93eb.js +0 -10
- package/dist/client-v2/302.d27fe4ea9b0b3bf5.js +0 -10
- package/dist/client-v2/562.fb2948ee6402de95.js +0 -10
- package/dist/client-v2/757.a117ce1cf7119cea.js +0 -10
- package/dist/client-v2/902.c7c00a565085438a.js +0 -10
- package/dist/server/resource/ai-api-user-permissions.js +0 -75
- package/src/client-v2/pages/UserPermissionsPage.tsx +0 -322
- package/src/client-v2/pages/UserQuotasPage.tsx +0 -276
- package/src/server/__tests__/user-permissions-resource.test.ts +0 -66
- package/src/server/resource/ai-api-user-permissions.ts +0 -76
|
@@ -0,0 +1,426 @@
|
|
|
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 __create = Object.create;
|
|
11
|
+
var __defProp = Object.defineProperty;
|
|
12
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
13
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
14
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
15
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
16
|
+
var __export = (target, all) => {
|
|
17
|
+
for (var name in all)
|
|
18
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
19
|
+
};
|
|
20
|
+
var __copyProps = (to, from, except, desc) => {
|
|
21
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
22
|
+
for (let key of __getOwnPropNames(from))
|
|
23
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
24
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
25
|
+
}
|
|
26
|
+
return to;
|
|
27
|
+
};
|
|
28
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
29
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
30
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
31
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
32
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
33
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
34
|
+
mod
|
|
35
|
+
));
|
|
36
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
37
|
+
var file_processor_exports = {};
|
|
38
|
+
__export(file_processor_exports, {
|
|
39
|
+
FileProcessorError: () => FileProcessorError,
|
|
40
|
+
FileProcessorService: () => FileProcessorService,
|
|
41
|
+
base64FileForwarder: () => base64FileForwarder,
|
|
42
|
+
fetchFileAsBase64: () => fetchFileAsBase64,
|
|
43
|
+
httpFileUrlFetcher: () => httpFileUrlFetcher,
|
|
44
|
+
isBlockedAddress: () => isBlockedAddress,
|
|
45
|
+
pdfFileProcessor: () => pdfFileProcessor
|
|
46
|
+
});
|
|
47
|
+
module.exports = __toCommonJS(file_processor_exports);
|
|
48
|
+
var import_dns = __toESM(require("dns"));
|
|
49
|
+
var import_net = require("net");
|
|
50
|
+
var import_path = require("path");
|
|
51
|
+
var import_request_cache = require("../utils/request-cache");
|
|
52
|
+
class FileProcessorError extends Error {
|
|
53
|
+
constructor(code, message) {
|
|
54
|
+
super(message);
|
|
55
|
+
this.code = code;
|
|
56
|
+
this.name = "FileProcessorError";
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const DEFAULT_MAX_FILE_SIZE = 50 * 1024 * 1024;
|
|
60
|
+
const DEFAULT_TIMEOUT_MS = 3e4;
|
|
61
|
+
const DEFAULT_MAX_REDIRECTS = 5;
|
|
62
|
+
const ALLOWED_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
|
|
63
|
+
const REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
|
|
64
|
+
class FileProcessorService {
|
|
65
|
+
processors = [];
|
|
66
|
+
pdfRenderer = null;
|
|
67
|
+
register(processor) {
|
|
68
|
+
this.unregister(processor.name);
|
|
69
|
+
this.processors.push(processor);
|
|
70
|
+
}
|
|
71
|
+
unregister(name) {
|
|
72
|
+
this.processors = this.processors.filter((p) => p.name !== name);
|
|
73
|
+
}
|
|
74
|
+
async process(block, context) {
|
|
75
|
+
const processor = [...this.processors].reverse().find((p) => p.canHandle(block));
|
|
76
|
+
if (!processor) {
|
|
77
|
+
return block;
|
|
78
|
+
}
|
|
79
|
+
return processor.process(block, context);
|
|
80
|
+
}
|
|
81
|
+
list() {
|
|
82
|
+
return [...this.processors];
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Register a renderer used to convert PDF pages into PNG images when
|
|
86
|
+
* `pdfRenderPagesAsImages` is enabled in the AI API config.
|
|
87
|
+
*/
|
|
88
|
+
registerPdfRenderer(renderer) {
|
|
89
|
+
this.pdfRenderer = renderer;
|
|
90
|
+
}
|
|
91
|
+
unregisterPdfRenderer() {
|
|
92
|
+
this.pdfRenderer = null;
|
|
93
|
+
}
|
|
94
|
+
getPdfRenderer() {
|
|
95
|
+
return this.pdfRenderer;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function isRecord(value) {
|
|
99
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
100
|
+
}
|
|
101
|
+
function extractFilename(url, contentDisposition) {
|
|
102
|
+
if (contentDisposition) {
|
|
103
|
+
const match = contentDisposition.match(/filename="?([^"]+)"?/);
|
|
104
|
+
if (match) return match[1];
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
const pathname = new URL(url).pathname;
|
|
108
|
+
if (pathname) return (0, import_path.basename)(pathname);
|
|
109
|
+
} catch {
|
|
110
|
+
}
|
|
111
|
+
return void 0;
|
|
112
|
+
}
|
|
113
|
+
function parseIpv4Octets(ip) {
|
|
114
|
+
const parts = ip.split(".");
|
|
115
|
+
if (parts.length !== 4) return void 0;
|
|
116
|
+
const octets = [];
|
|
117
|
+
for (const part of parts) {
|
|
118
|
+
if (!/^\d{1,3}$/.test(part)) return void 0;
|
|
119
|
+
const value = Number(part);
|
|
120
|
+
if (value > 255) return void 0;
|
|
121
|
+
octets.push(value);
|
|
122
|
+
}
|
|
123
|
+
return octets;
|
|
124
|
+
}
|
|
125
|
+
function isBlockedIpv4(ip) {
|
|
126
|
+
const octets = parseIpv4Octets(ip);
|
|
127
|
+
if (!octets) return true;
|
|
128
|
+
const [a, b] = octets;
|
|
129
|
+
if (a === 0) return true;
|
|
130
|
+
if (a === 10) return true;
|
|
131
|
+
if (a === 100 && b >= 64 && b <= 127) return true;
|
|
132
|
+
if (a === 127) return true;
|
|
133
|
+
if (a === 169 && b === 254) return true;
|
|
134
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
135
|
+
if (a === 192 && b === 0) return true;
|
|
136
|
+
if (a === 192 && b === 168) return true;
|
|
137
|
+
if (a === 198 && (b === 18 || b === 19)) return true;
|
|
138
|
+
if (a >= 224) return true;
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
function parseHexGroup(group) {
|
|
142
|
+
if (group.length < 1 || group.length > 4 || !/^[0-9a-fA-F]+$/.test(group)) return void 0;
|
|
143
|
+
return parseInt(group, 16);
|
|
144
|
+
}
|
|
145
|
+
function expandIpv6(input) {
|
|
146
|
+
let address = input;
|
|
147
|
+
const zoneIndex = address.indexOf("%");
|
|
148
|
+
if (zoneIndex !== -1) address = address.slice(0, zoneIndex);
|
|
149
|
+
if (!address) return void 0;
|
|
150
|
+
const lastColon = address.lastIndexOf(":");
|
|
151
|
+
if (lastColon !== -1 && address.includes(".", lastColon)) {
|
|
152
|
+
const octets = parseIpv4Octets(address.slice(lastColon + 1));
|
|
153
|
+
if (!octets) return void 0;
|
|
154
|
+
const high = (octets[0] << 8 | octets[1]).toString(16);
|
|
155
|
+
const low = (octets[2] << 8 | octets[3]).toString(16);
|
|
156
|
+
address = `${address.slice(0, lastColon + 1)}${high}:${low}`;
|
|
157
|
+
}
|
|
158
|
+
const groups = [];
|
|
159
|
+
const doubleColonIndex = address.indexOf("::");
|
|
160
|
+
if (doubleColonIndex !== -1) {
|
|
161
|
+
if (address.indexOf("::", doubleColonIndex + 1) !== -1) return void 0;
|
|
162
|
+
const head = address.slice(0, doubleColonIndex);
|
|
163
|
+
const tail = address.slice(doubleColonIndex + 2);
|
|
164
|
+
const headGroups = head === "" ? [] : head.split(":");
|
|
165
|
+
const tailGroups = tail === "" ? [] : tail.split(":");
|
|
166
|
+
const fillCount = 8 - headGroups.length - tailGroups.length;
|
|
167
|
+
if (fillCount < 1) return void 0;
|
|
168
|
+
const allGroups = [...headGroups];
|
|
169
|
+
for (let i = 0; i < fillCount; i += 1) allGroups.push("0");
|
|
170
|
+
allGroups.push(...tailGroups);
|
|
171
|
+
for (const group of allGroups) {
|
|
172
|
+
const parsed = parseHexGroup(group);
|
|
173
|
+
if (parsed === void 0) return void 0;
|
|
174
|
+
groups.push(parsed);
|
|
175
|
+
}
|
|
176
|
+
} else {
|
|
177
|
+
for (const group of address.split(":")) {
|
|
178
|
+
const parsed = parseHexGroup(group);
|
|
179
|
+
if (parsed === void 0) return void 0;
|
|
180
|
+
groups.push(parsed);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return groups.length === 8 ? groups : void 0;
|
|
184
|
+
}
|
|
185
|
+
function isBlockedIpv6(ip) {
|
|
186
|
+
const groups = expandIpv6(ip);
|
|
187
|
+
if (!groups) return true;
|
|
188
|
+
const [g0, g1, g2, g3, g4, g5, g6, g7] = groups;
|
|
189
|
+
if (groups.every((group) => group === 0)) return true;
|
|
190
|
+
if (g0 === 0 && g1 === 0 && g2 === 0 && g3 === 0 && g4 === 0 && g5 === 0 && g6 === 0 && g7 === 1) {
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
const isIpv4Mapped = g0 === 0 && g1 === 0 && g2 === 0 && g3 === 0 && g4 === 0 && g5 === 65535;
|
|
194
|
+
const isIpv4Compatible = g0 === 0 && g1 === 0 && g2 === 0 && g3 === 0 && g4 === 0 && g5 === 0;
|
|
195
|
+
const isNat64 = g0 === 100 && g1 === 65435 && g2 === 0 && g3 === 0 && g4 === 0 && g5 === 0;
|
|
196
|
+
if (isIpv4Mapped || isIpv4Compatible || isNat64) {
|
|
197
|
+
const embedded = `${g6 >> 8 & 255}.${g6 & 255}.${g7 >> 8 & 255}.${g7 & 255}`;
|
|
198
|
+
return isBlockedIpv4(embedded);
|
|
199
|
+
}
|
|
200
|
+
if ((g0 & 65024) === 64512) return true;
|
|
201
|
+
if ((g0 & 65472) === 65152) return true;
|
|
202
|
+
if ((g0 & 65280) === 65280) return true;
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
function isBlockedAddress(ip) {
|
|
206
|
+
const version = (0, import_net.isIP)(ip);
|
|
207
|
+
if (version === 4) return isBlockedIpv4(ip);
|
|
208
|
+
if (version === 6) return isBlockedIpv6(ip);
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
async function assertHostAllowed(hostname) {
|
|
212
|
+
if ((0, import_net.isIP)(hostname) !== 0) {
|
|
213
|
+
if (isBlockedAddress(hostname)) {
|
|
214
|
+
throw new FileProcessorError("blocked_host", `Host '${hostname}' is a blocked address.`);
|
|
215
|
+
}
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
let addresses;
|
|
219
|
+
try {
|
|
220
|
+
addresses = await import_dns.default.promises.lookup(hostname, { all: true });
|
|
221
|
+
} catch (error) {
|
|
222
|
+
const message = error instanceof Error && error.message ? error.message : String(error);
|
|
223
|
+
throw new FileProcessorError("fetch_failed", `Could not resolve host '${hostname}': ${message}`);
|
|
224
|
+
}
|
|
225
|
+
if (!addresses.length) {
|
|
226
|
+
throw new FileProcessorError("fetch_failed", `Could not resolve host '${hostname}'.`);
|
|
227
|
+
}
|
|
228
|
+
for (const { address } of addresses) {
|
|
229
|
+
if (isBlockedAddress(address)) {
|
|
230
|
+
throw new FileProcessorError("blocked_host", `Host '${hostname}' resolves to blocked address '${address}'.`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
async function fetchWithSsrfGuard(initialUrl, options) {
|
|
235
|
+
let currentUrl = initialUrl;
|
|
236
|
+
for (let hop = 0; ; hop += 1) {
|
|
237
|
+
let parsed;
|
|
238
|
+
try {
|
|
239
|
+
parsed = new URL(currentUrl);
|
|
240
|
+
} catch {
|
|
241
|
+
throw new FileProcessorError("invalid_url", `File URL '${currentUrl}' is not a valid URL.`);
|
|
242
|
+
}
|
|
243
|
+
if (!options.allowedProtocols.has(parsed.protocol)) {
|
|
244
|
+
throw new FileProcessorError("unsupported_protocol", `File URL protocol '${parsed.protocol}' is not allowed.`);
|
|
245
|
+
}
|
|
246
|
+
const hostname = parsed.hostname.startsWith("[") && parsed.hostname.endsWith("]") ? parsed.hostname.slice(1, -1) : parsed.hostname;
|
|
247
|
+
await assertHostAllowed(hostname);
|
|
248
|
+
const response = await fetch(currentUrl, { signal: options.signal, redirect: "manual" });
|
|
249
|
+
if (!REDIRECT_STATUSES.has(response.status)) {
|
|
250
|
+
return { response, finalUrl: currentUrl };
|
|
251
|
+
}
|
|
252
|
+
if (hop >= options.maxRedirects) {
|
|
253
|
+
throw new FileProcessorError(
|
|
254
|
+
"too_many_redirects",
|
|
255
|
+
`File URL '${initialUrl}' exceeded the limit of ${options.maxRedirects} redirects.`
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
const location = response.headers.get("location");
|
|
259
|
+
if (!location) {
|
|
260
|
+
throw new FileProcessorError("fetch_failed", `Redirect from '${currentUrl}' is missing a Location header.`);
|
|
261
|
+
}
|
|
262
|
+
try {
|
|
263
|
+
currentUrl = new URL(location, currentUrl).toString();
|
|
264
|
+
} catch {
|
|
265
|
+
throw new FileProcessorError(
|
|
266
|
+
"invalid_url",
|
|
267
|
+
`Redirect Location '${location}' from '${currentUrl}' is not a valid URL.`
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
async function fetchFileAsBase64(url, options = {}) {
|
|
273
|
+
const maxSize = options.maxSizeBytes ?? DEFAULT_MAX_FILE_SIZE;
|
|
274
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
275
|
+
const maxRedirects = options.maxRedirects ?? DEFAULT_MAX_REDIRECTS;
|
|
276
|
+
const allowedProtocols = options.allowedProtocols ? new Set(options.allowedProtocols) : ALLOWED_PROTOCOLS;
|
|
277
|
+
const controller = new AbortController();
|
|
278
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
279
|
+
try {
|
|
280
|
+
const { response, finalUrl } = await fetchWithSsrfGuard(url, {
|
|
281
|
+
allowedProtocols,
|
|
282
|
+
maxRedirects,
|
|
283
|
+
signal: controller.signal
|
|
284
|
+
});
|
|
285
|
+
if (!response.ok) {
|
|
286
|
+
throw new FileProcessorError(
|
|
287
|
+
"fetch_failed",
|
|
288
|
+
`Failed to fetch file from '${finalUrl}': ${response.status} ${response.statusText}`
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
const contentLength = response.headers.get("content-length");
|
|
292
|
+
if (contentLength && Number(contentLength) > maxSize) {
|
|
293
|
+
throw new FileProcessorError("file_too_large", `File at '${finalUrl}' exceeds maximum allowed size.`);
|
|
294
|
+
}
|
|
295
|
+
const contentType = response.headers.get("content-type") || void 0;
|
|
296
|
+
if (options.allowedContentTypes && contentType && !options.allowedContentTypes.some((type) => contentType.includes(type))) {
|
|
297
|
+
throw new FileProcessorError("content_type_not_allowed", `File content type '${contentType}' is not allowed.`);
|
|
298
|
+
}
|
|
299
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
300
|
+
if (buffer.length > maxSize) {
|
|
301
|
+
throw new FileProcessorError("file_too_large", `File at '${finalUrl}' exceeds maximum allowed size.`);
|
|
302
|
+
}
|
|
303
|
+
const mimeType = (contentType == null ? void 0 : contentType.split(";")[0].trim()) ?? "application/octet-stream";
|
|
304
|
+
const contentDisposition = response.headers.get("content-disposition");
|
|
305
|
+
const filename = extractFilename(finalUrl, contentDisposition) ?? "file";
|
|
306
|
+
return {
|
|
307
|
+
fileData: `data:${mimeType};base64,${buffer.toString("base64")}`,
|
|
308
|
+
mimeType,
|
|
309
|
+
filename
|
|
310
|
+
};
|
|
311
|
+
} finally {
|
|
312
|
+
clearTimeout(timeout);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
const base64FileForwarder = {
|
|
316
|
+
name: "base64FileForwarder",
|
|
317
|
+
canHandle(block) {
|
|
318
|
+
if (block.type !== "file") return false;
|
|
319
|
+
const file = isRecord(block.file) ? block.file : void 0;
|
|
320
|
+
if (!file) return false;
|
|
321
|
+
const fileData = String(file.file_data ?? "");
|
|
322
|
+
return fileData.startsWith("data:") && fileData.includes(";base64,");
|
|
323
|
+
},
|
|
324
|
+
async process(block) {
|
|
325
|
+
return block;
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
const httpFileUrlFetcher = {
|
|
329
|
+
name: "httpFileUrlFetcher",
|
|
330
|
+
canHandle(block) {
|
|
331
|
+
if (block.type !== "file_url") return false;
|
|
332
|
+
const fileUrl = isRecord(block.file_url) ? block.file_url : void 0;
|
|
333
|
+
if (!fileUrl) return false;
|
|
334
|
+
const url = String(fileUrl.url ?? "");
|
|
335
|
+
return url.startsWith("http://") || url.startsWith("https://");
|
|
336
|
+
},
|
|
337
|
+
async process(block) {
|
|
338
|
+
const fileUrl = isRecord(block.file_url) ? block.file_url : void 0;
|
|
339
|
+
const url = String((fileUrl == null ? void 0 : fileUrl.url) ?? "");
|
|
340
|
+
if (!url) {
|
|
341
|
+
throw new FileProcessorError("missing_url", "file_url block requires a 'url' property.");
|
|
342
|
+
}
|
|
343
|
+
const { fileData, mimeType, filename } = await fetchFileAsBase64(url);
|
|
344
|
+
return {
|
|
345
|
+
type: "file",
|
|
346
|
+
file: {
|
|
347
|
+
file_data: fileData,
|
|
348
|
+
mime_type: mimeType,
|
|
349
|
+
filename: filename || "file"
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
function decodeBase64DataUrl(url) {
|
|
355
|
+
const match = /^data:([^;]+);base64,([A-Za-z0-9+/]+=*)$/.exec(url);
|
|
356
|
+
if (!match) return void 0;
|
|
357
|
+
try {
|
|
358
|
+
const buffer = Buffer.from(match[2], "base64");
|
|
359
|
+
return { mimeType: match[1].toLowerCase(), buffer };
|
|
360
|
+
} catch {
|
|
361
|
+
return void 0;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
function isPdfBuffer(buffer) {
|
|
365
|
+
return buffer.length >= 4 && buffer.toString("binary", 0, 4) === "%PDF";
|
|
366
|
+
}
|
|
367
|
+
function isPdfFileBlock(block) {
|
|
368
|
+
if (block.type !== "file") return false;
|
|
369
|
+
const file = isRecord(block.file) ? block.file : void 0;
|
|
370
|
+
if (!file) return false;
|
|
371
|
+
const fileData = String(file.file_data ?? "");
|
|
372
|
+
if (!fileData.startsWith("data:")) return false;
|
|
373
|
+
const mimeType = String(file.mime_type ?? "").toLowerCase();
|
|
374
|
+
if (mimeType === "application/pdf") return true;
|
|
375
|
+
if (fileData.startsWith("data:application/pdf")) return true;
|
|
376
|
+
const decoded = decodeBase64DataUrl(fileData);
|
|
377
|
+
if (decoded && isPdfBuffer(decoded.buffer)) return true;
|
|
378
|
+
return false;
|
|
379
|
+
}
|
|
380
|
+
function getPluginFromContext(context) {
|
|
381
|
+
var _a, _b, _c;
|
|
382
|
+
return (_c = (_b = (_a = context.ctx.app) == null ? void 0 : _a.pm) == null ? void 0 : _b.get) == null ? void 0 : _c.call(_b, "plugin-ai-api");
|
|
383
|
+
}
|
|
384
|
+
const pdfFileProcessor = {
|
|
385
|
+
name: "pdfFileProcessor",
|
|
386
|
+
canHandle(block) {
|
|
387
|
+
return isPdfFileBlock(block);
|
|
388
|
+
},
|
|
389
|
+
async process(block, context) {
|
|
390
|
+
var _a, _b, _c, _d;
|
|
391
|
+
const config = await (0, import_request_cache.getAiApiConfig)(context.ctx);
|
|
392
|
+
if (!(config == null ? void 0 : config.pdfRenderPagesAsImages)) {
|
|
393
|
+
return block;
|
|
394
|
+
}
|
|
395
|
+
const plugin = getPluginFromContext(context);
|
|
396
|
+
const renderer = (_b = (_a = plugin == null ? void 0 : plugin.fileProcessorService) == null ? void 0 : _a.getPdfRenderer) == null ? void 0 : _b.call(_a);
|
|
397
|
+
if (!renderer) {
|
|
398
|
+
(_d = (_c = context.ctx.log) == null ? void 0 : _c.warn) == null ? void 0 : _d.call(
|
|
399
|
+
_c,
|
|
400
|
+
"[pdfFileProcessor] pdfRenderPagesAsImages is enabled but no PdfToImageRenderer is registered. Forwarding PDF as a file block."
|
|
401
|
+
);
|
|
402
|
+
return block;
|
|
403
|
+
}
|
|
404
|
+
const file = isRecord(block.file) ? block.file : void 0;
|
|
405
|
+
const fileData = String((file == null ? void 0 : file.file_data) ?? "");
|
|
406
|
+
const decoded = decodeBase64DataUrl(fileData);
|
|
407
|
+
if (!decoded) {
|
|
408
|
+
return block;
|
|
409
|
+
}
|
|
410
|
+
const pages = await renderer.render(decoded.buffer);
|
|
411
|
+
return pages.map((buffer) => ({
|
|
412
|
+
type: "image_url",
|
|
413
|
+
image_url: { url: `data:image/png;base64,${buffer.toString("base64")}` }
|
|
414
|
+
}));
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
418
|
+
0 && (module.exports = {
|
|
419
|
+
FileProcessorError,
|
|
420
|
+
FileProcessorService,
|
|
421
|
+
base64FileForwarder,
|
|
422
|
+
fetchFileAsBase64,
|
|
423
|
+
httpFileUrlFetcher,
|
|
424
|
+
isBlockedAddress,
|
|
425
|
+
pdfFileProcessor
|
|
426
|
+
});
|
package/dist/server/usage.js
CHANGED
|
@@ -42,6 +42,24 @@ function getAiApiState(ctx) {
|
|
|
42
42
|
function normalizeTokenCount(value) {
|
|
43
43
|
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null;
|
|
44
44
|
}
|
|
45
|
+
function extractPromptCacheTokens(source) {
|
|
46
|
+
const inputDetails = source.input_token_details;
|
|
47
|
+
if (inputDetails && typeof inputDetails === "object") {
|
|
48
|
+
const cacheRead = inputDetails.cache_read;
|
|
49
|
+
if (typeof cacheRead === "number") return cacheRead;
|
|
50
|
+
const cachedTokens = inputDetails.cached_tokens;
|
|
51
|
+
if (typeof cachedTokens === "number") return cachedTokens;
|
|
52
|
+
}
|
|
53
|
+
const promptDetails = source.prompt_tokens_details;
|
|
54
|
+
if (promptDetails && typeof promptDetails === "object") {
|
|
55
|
+
const cachedTokens = promptDetails.cached_tokens;
|
|
56
|
+
if (typeof cachedTokens === "number") return cachedTokens;
|
|
57
|
+
}
|
|
58
|
+
if (typeof source.cached_tokens === "number") return source.cached_tokens;
|
|
59
|
+
if (typeof source.cache_read_tokens === "number") return source.cache_read_tokens;
|
|
60
|
+
if (typeof source.cache_read === "number") return source.cache_read;
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
45
63
|
function normalizeUsage(value) {
|
|
46
64
|
if (!value || typeof value !== "object") return void 0;
|
|
47
65
|
const source = value;
|
|
@@ -55,11 +73,25 @@ function normalizeUsage(value) {
|
|
|
55
73
|
return {
|
|
56
74
|
prompt_tokens: prompt,
|
|
57
75
|
completion_tokens: completion,
|
|
58
|
-
total_tokens: total
|
|
76
|
+
total_tokens: total,
|
|
77
|
+
// Keep normalizeUsage idempotent: streaming routes normalize the chunk
|
|
78
|
+
// usage once and hand the result to setAiApiUsageResult, which normalizes
|
|
79
|
+
// again — an already-extracted prompt_cache_tokens must survive that pass.
|
|
80
|
+
prompt_cache_tokens: extractPromptCacheTokens(source) ?? normalizeTokenCount(source.prompt_cache_tokens)
|
|
59
81
|
};
|
|
60
82
|
}
|
|
61
|
-
function setAiApiUsageResult(ctx, value, metadata = {}) {
|
|
62
|
-
|
|
83
|
+
function setAiApiUsageResult(ctx, value, metadata = {}, responseMetadata) {
|
|
84
|
+
let usage = normalizeUsage(value);
|
|
85
|
+
if ((usage == null ? void 0 : usage.prompt_cache_tokens) === null && responseMetadata && typeof responseMetadata === "object") {
|
|
86
|
+
const responseMetaRecord = responseMetadata;
|
|
87
|
+
let cacheTokens = extractPromptCacheTokens(responseMetaRecord);
|
|
88
|
+
if (cacheTokens === null && responseMetaRecord.usage && typeof responseMetaRecord.usage === "object") {
|
|
89
|
+
cacheTokens = extractPromptCacheTokens(responseMetaRecord.usage);
|
|
90
|
+
}
|
|
91
|
+
if (cacheTokens !== null) {
|
|
92
|
+
usage = { ...usage, prompt_cache_tokens: cacheTokens };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
63
95
|
getAiApiState(ctx).aiApiUsageResult = usage ? { source: "provider", usage, ...metadata } : { source: "unavailable", ...metadata };
|
|
64
96
|
(0, import_app_observability.addAiApiUsage)(ctx, usage);
|
|
65
97
|
return usage;
|
|
@@ -133,6 +165,7 @@ async function finishUsageRecord(ctx, id, startedAt, status) {
|
|
|
133
165
|
inputTokens: (usage == null ? void 0 : usage.prompt_tokens) ?? null,
|
|
134
166
|
outputTokens: (usage == null ? void 0 : usage.completion_tokens) ?? null,
|
|
135
167
|
totalTokens: (usage == null ? void 0 : usage.total_tokens) ?? null,
|
|
168
|
+
promptCacheTokens: (usage == null ? void 0 : usage.prompt_cache_tokens) ?? null,
|
|
136
169
|
resolvedService: ((_c = (_b = state.aiApiLlmBilling) == null ? void 0 : _b.resolution) == null ? void 0 : _c.service) ?? null,
|
|
137
170
|
resolvedProvider: ((_e = (_d = state.aiApiLlmBilling) == null ? void 0 : _d.resolution) == null ? void 0 : _e.provider) ?? null,
|
|
138
171
|
resolvedModel: ((_g = (_f = state.aiApiLlmBilling) == null ? void 0 : _f.resolution) == null ? void 0 : _g.model) ?? null,
|
|
@@ -141,6 +174,7 @@ async function finishUsageRecord(ctx, id, startedAt, status) {
|
|
|
141
174
|
costStatus: billing.costStatus ?? null,
|
|
142
175
|
modelPriceId: billing.modelPriceId ?? null,
|
|
143
176
|
quotaPolicyId: billing.quotaPolicyId ?? null,
|
|
177
|
+
groupId: billing.groupId ?? null,
|
|
144
178
|
inputPricePerMillionTokens: billing.inputPricePerMillionTokens ?? null,
|
|
145
179
|
outputPricePerMillionTokens: billing.outputPricePerMillionTokens ?? null,
|
|
146
180
|
fixedCostPerRequest: billing.fixedCostPerRequest ?? null,
|