plugin-ai-api 1.0.13 → 1.0.14
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/externalVersion.js +10 -9
- package/dist/server/collections/ai-api-usage-records.js +9 -1
- package/dist/server/migrations/20260727140000-change-usage-user-id-to-bigint.js +208 -0
- package/dist/server/routes/agent-completions.js +4 -0
- package/dist/server/routes/auth.js +1 -0
- package/dist/server/routes/chat-completions.js +13 -13
- package/dist/server/routes/completions.js +13 -13
- package/dist/server/routes/embeddings.js +2 -0
- package/dist/server/routes/router.js +12 -11
- package/dist/server/usage.js +75 -21
- package/package.json +1 -1
- package/src/server/__tests__/usage-migration.test.ts +171 -0
- package/src/server/__tests__/usage-route.test.ts +105 -0
- package/src/server/__tests__/usage.test.ts +201 -0
- package/src/server/collections/ai-api-usage-records.ts +9 -1
- package/src/server/migrations/20260727140000-change-usage-user-id-to-bigint.ts +222 -0
- package/src/server/routes/agent-completions.ts +4 -0
- package/src/server/routes/auth.ts +1 -0
- package/src/server/routes/chat-completions.ts +14 -16
- package/src/server/routes/completions.ts +14 -16
- package/src/server/routes/embeddings.ts +2 -0
- package/src/server/routes/router.ts +14 -10
- package/src/server/usage.ts +129 -28
- package/src/server/utils/openai-format.ts +6 -2
package/dist/externalVersion.js
CHANGED
|
@@ -10,14 +10,15 @@
|
|
|
10
10
|
module.exports = {
|
|
11
11
|
"react": "18.2.0",
|
|
12
12
|
"antd": "5.24.2",
|
|
13
|
-
"@nocobase/client-v2": "2.1.
|
|
14
|
-
"@nocobase/flow-engine": "2.1.
|
|
15
|
-
"@nocobase/client": "2.1.
|
|
16
|
-
"@nocobase/plugin-acl": "2.1.
|
|
17
|
-
"@nocobase/server": "2.1.
|
|
13
|
+
"@nocobase/client-v2": "2.1.30",
|
|
14
|
+
"@nocobase/flow-engine": "2.1.30",
|
|
15
|
+
"@nocobase/client": "2.1.30",
|
|
16
|
+
"@nocobase/plugin-acl": "2.1.30",
|
|
17
|
+
"@nocobase/server": "2.1.30",
|
|
18
18
|
"dayjs": "1.11.13",
|
|
19
|
-
"@nocobase/actions": "2.1.
|
|
20
|
-
"@nocobase/database": "2.1.
|
|
21
|
-
"
|
|
22
|
-
"@nocobase/
|
|
19
|
+
"@nocobase/actions": "2.1.30",
|
|
20
|
+
"@nocobase/database": "2.1.30",
|
|
21
|
+
"sequelize": "6.35.2",
|
|
22
|
+
"@nocobase/resourcer": "2.1.30",
|
|
23
|
+
"@nocobase/plugin-ai": "2.1.30"
|
|
23
24
|
};
|
|
@@ -35,7 +35,15 @@ var ai_api_usage_records_default = (0, import_database.defineCollection)({
|
|
|
35
35
|
autoGenId: true,
|
|
36
36
|
fields: [
|
|
37
37
|
{ name: "requestId", type: "string", unique: true, index: true },
|
|
38
|
-
{ name: "userId", type: "
|
|
38
|
+
{ name: "userId", type: "bigInt", allowNull: false, index: true },
|
|
39
|
+
{
|
|
40
|
+
name: "user",
|
|
41
|
+
type: "belongsTo",
|
|
42
|
+
target: "users",
|
|
43
|
+
targetKey: "id",
|
|
44
|
+
foreignKey: "userId",
|
|
45
|
+
constraints: false
|
|
46
|
+
},
|
|
39
47
|
{ name: "roleName", type: "string", index: true },
|
|
40
48
|
{ name: "authType", type: "string", index: true },
|
|
41
49
|
{ name: "oauthClientId", type: "string", allowNull: true, index: true },
|
|
@@ -0,0 +1,208 @@
|
|
|
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 change_usage_user_id_to_bigint_exports = {};
|
|
28
|
+
__export(change_usage_user_id_to_bigint_exports, {
|
|
29
|
+
default: () => ChangeUsageUserIdToBigInt,
|
|
30
|
+
isBigIntColumnType: () => isBigIntColumnType,
|
|
31
|
+
isLegacyStringColumnType: () => isLegacyStringColumnType,
|
|
32
|
+
normalizeLegacyUserId: () => normalizeLegacyUserId
|
|
33
|
+
});
|
|
34
|
+
module.exports = __toCommonJS(change_usage_user_id_to_bigint_exports);
|
|
35
|
+
var import_database = require("@nocobase/database");
|
|
36
|
+
var import_server = require("@nocobase/server");
|
|
37
|
+
var import_sequelize = require("sequelize");
|
|
38
|
+
const MAX_SIGNED_BIGINT = 9223372036854775807n;
|
|
39
|
+
const PREFLIGHT_BATCH_SIZE = 1e3;
|
|
40
|
+
function stringifyDatabaseValue(value) {
|
|
41
|
+
if (typeof value === "bigint") return value.toString();
|
|
42
|
+
if (value === null) return "null";
|
|
43
|
+
if (value === void 0) return "undefined";
|
|
44
|
+
return String(value);
|
|
45
|
+
}
|
|
46
|
+
function normalizeLegacyUserId(value) {
|
|
47
|
+
let normalized;
|
|
48
|
+
if (typeof value === "string") {
|
|
49
|
+
normalized = value.trim();
|
|
50
|
+
} else if (typeof value === "bigint") {
|
|
51
|
+
normalized = value.toString();
|
|
52
|
+
} else if (typeof value === "number" && Number.isSafeInteger(value)) {
|
|
53
|
+
normalized = String(value);
|
|
54
|
+
} else {
|
|
55
|
+
return void 0;
|
|
56
|
+
}
|
|
57
|
+
if (!/^\d+$/.test(normalized)) return void 0;
|
|
58
|
+
const parsed = BigInt(normalized);
|
|
59
|
+
if (parsed > MAX_SIGNED_BIGINT) return void 0;
|
|
60
|
+
return normalized;
|
|
61
|
+
}
|
|
62
|
+
function isBigIntColumnType(value) {
|
|
63
|
+
return typeof value === "string" && value.toUpperCase().includes("BIGINT");
|
|
64
|
+
}
|
|
65
|
+
function isLegacyStringColumnType(value) {
|
|
66
|
+
if (typeof value !== "string") return false;
|
|
67
|
+
const normalized = value.toUpperCase();
|
|
68
|
+
return normalized.includes("CHAR") || normalized.includes("TEXT");
|
|
69
|
+
}
|
|
70
|
+
class ChangeUsageUserIdToBigInt extends import_server.Migration {
|
|
71
|
+
on = "beforeLoad";
|
|
72
|
+
async up() {
|
|
73
|
+
const { collection, temporary } = this.getUsageCollection();
|
|
74
|
+
try {
|
|
75
|
+
if (!await collection.existsInDb()) return;
|
|
76
|
+
const userIdField = collection.getField("userId");
|
|
77
|
+
if (!userIdField) {
|
|
78
|
+
throw new Error("AI API usage migration could not resolve the userId field.");
|
|
79
|
+
}
|
|
80
|
+
const tableName = collection.getTableNameWithSchema();
|
|
81
|
+
const columnName = userIdField.columnName();
|
|
82
|
+
const columns = await this.queryInterface.describeTable(tableName);
|
|
83
|
+
const column = columns[columnName];
|
|
84
|
+
if (!column) {
|
|
85
|
+
throw new Error(`AI API usage migration could not find the physical column ${columnName}.`);
|
|
86
|
+
}
|
|
87
|
+
if (!isBigIntColumnType(column.type) && !isLegacyStringColumnType(column.type)) {
|
|
88
|
+
throw new Error(`AI API usage migration does not support converting ${column.type} to BIGINT.`);
|
|
89
|
+
}
|
|
90
|
+
const invalidRows = await this.findInvalidRows(collection.quotedTableName(), columnName);
|
|
91
|
+
if (invalidRows.count > 0) {
|
|
92
|
+
const samples = invalidRows.samples.map((row) => `${row.recordId}:${JSON.stringify(row.userId)}`).join(", ");
|
|
93
|
+
throw new Error(
|
|
94
|
+
`AI API usage migration found ${invalidRows.count} invalid userId value(s). Clean these records before retrying the upgrade. Samples: ${samples}`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
if (isBigIntColumnType(column.type)) {
|
|
98
|
+
if (column.allowNull === false) return;
|
|
99
|
+
await this.makeColumnNotNull(tableName, collection.quotedTableName(), columnName);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
await this.changeColumnToBigInt(tableName, collection.quotedTableName(), columnName);
|
|
103
|
+
} finally {
|
|
104
|
+
if (temporary) this.db.removeCollection("aiApiUsageRecords");
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
async down() {
|
|
108
|
+
var _a;
|
|
109
|
+
const { collection, temporary } = this.getUsageCollection();
|
|
110
|
+
try {
|
|
111
|
+
if (!await collection.existsInDb()) return;
|
|
112
|
+
const userIdField = collection.getField("userId");
|
|
113
|
+
if (!userIdField) return;
|
|
114
|
+
const tableName = collection.getTableNameWithSchema();
|
|
115
|
+
const columnName = userIdField.columnName();
|
|
116
|
+
const columns = await this.queryInterface.describeTable(tableName);
|
|
117
|
+
if (!isBigIntColumnType((_a = columns[columnName]) == null ? void 0 : _a.type)) return;
|
|
118
|
+
if (this.db.inDialect("postgres")) {
|
|
119
|
+
const quotedColumn = this.db.quoteIdentifier(columnName);
|
|
120
|
+
await this.sequelize.transaction(async (transaction) => {
|
|
121
|
+
await this.sequelize.query(
|
|
122
|
+
`ALTER TABLE ${collection.quotedTableName()} ALTER COLUMN ${quotedColumn} DROP NOT NULL, ALTER COLUMN ${quotedColumn} TYPE VARCHAR(255) USING ${quotedColumn}::text`,
|
|
123
|
+
{ transaction }
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
await this.queryInterface.changeColumn(tableName, columnName, {
|
|
129
|
+
type: import_database.DataTypes.STRING,
|
|
130
|
+
allowNull: true
|
|
131
|
+
});
|
|
132
|
+
} finally {
|
|
133
|
+
if (temporary) this.db.removeCollection("aiApiUsageRecords");
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
getUsageCollection() {
|
|
137
|
+
const existing = this.db.getCollection("aiApiUsageRecords");
|
|
138
|
+
if (existing) return { collection: existing, temporary: false };
|
|
139
|
+
const collection = this.db.collection({
|
|
140
|
+
name: "aiApiUsageRecords",
|
|
141
|
+
fields: [{ name: "userId", type: "string" }]
|
|
142
|
+
});
|
|
143
|
+
return { collection, temporary: true };
|
|
144
|
+
}
|
|
145
|
+
async findInvalidRows(quotedTableName, columnName) {
|
|
146
|
+
const quotedId = this.db.quoteIdentifier("id");
|
|
147
|
+
const quotedUserId = this.db.quoteIdentifier(columnName);
|
|
148
|
+
const invalidRows = { count: 0, samples: [] };
|
|
149
|
+
let offset = 0;
|
|
150
|
+
let hasMoreRows = true;
|
|
151
|
+
while (hasMoreRows) {
|
|
152
|
+
const rows = await this.sequelize.query(
|
|
153
|
+
`SELECT ${quotedId} AS ${this.db.quoteIdentifier("recordId")}, ${quotedUserId} AS ${this.db.quoteIdentifier("userId")} FROM ${quotedTableName} ORDER BY ${quotedId} ASC LIMIT :limit OFFSET :offset`,
|
|
154
|
+
{
|
|
155
|
+
replacements: { limit: PREFLIGHT_BATCH_SIZE, offset },
|
|
156
|
+
type: import_sequelize.QueryTypes.SELECT
|
|
157
|
+
}
|
|
158
|
+
);
|
|
159
|
+
for (const row of rows) {
|
|
160
|
+
if (normalizeLegacyUserId(row.userId) === void 0) {
|
|
161
|
+
invalidRows.count += 1;
|
|
162
|
+
if (invalidRows.samples.length < 20) {
|
|
163
|
+
invalidRows.samples.push({
|
|
164
|
+
recordId: stringifyDatabaseValue(row.recordId),
|
|
165
|
+
userId: stringifyDatabaseValue(row.userId)
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
hasMoreRows = rows.length === PREFLIGHT_BATCH_SIZE;
|
|
171
|
+
offset += rows.length;
|
|
172
|
+
}
|
|
173
|
+
return invalidRows;
|
|
174
|
+
}
|
|
175
|
+
async changeColumnToBigInt(tableName, quotedTableName, columnName) {
|
|
176
|
+
if (this.db.inDialect("postgres")) {
|
|
177
|
+
const quotedColumn = this.db.quoteIdentifier(columnName);
|
|
178
|
+
await this.sequelize.transaction(async (transaction) => {
|
|
179
|
+
await this.sequelize.query(
|
|
180
|
+
`ALTER TABLE ${quotedTableName} ALTER COLUMN ${quotedColumn} TYPE BIGINT USING BTRIM(${quotedColumn})::BIGINT, ALTER COLUMN ${quotedColumn} SET NOT NULL`,
|
|
181
|
+
{ transaction }
|
|
182
|
+
);
|
|
183
|
+
});
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
await this.queryInterface.changeColumn(tableName, columnName, {
|
|
187
|
+
type: import_database.DataTypes.BIGINT,
|
|
188
|
+
allowNull: false
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
async makeColumnNotNull(tableName, quotedTableName, columnName) {
|
|
192
|
+
if (this.db.inDialect("postgres")) {
|
|
193
|
+
const quotedColumn = this.db.quoteIdentifier(columnName);
|
|
194
|
+
await this.sequelize.query(`ALTER TABLE ${quotedTableName} ALTER COLUMN ${quotedColumn} SET NOT NULL`);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
await this.queryInterface.changeColumn(tableName, columnName, {
|
|
198
|
+
type: import_database.DataTypes.BIGINT,
|
|
199
|
+
allowNull: false
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
204
|
+
0 && (module.exports = {
|
|
205
|
+
isBigIntColumnType,
|
|
206
|
+
isLegacyStringColumnType,
|
|
207
|
+
normalizeLegacyUserId
|
|
208
|
+
});
|
|
@@ -34,6 +34,7 @@ var import_resolve_service = require("../utils/resolve-service");
|
|
|
34
34
|
var import_role_permission = require("../middleware/role-permission");
|
|
35
35
|
var import_streaming = require("../utils/streaming");
|
|
36
36
|
var import_ai_employee_runtime = require("../utils/ai-employee-runtime");
|
|
37
|
+
var import_usage = require("../usage");
|
|
37
38
|
async function handleAgentCompletions(ctx, plugin) {
|
|
38
39
|
var _a;
|
|
39
40
|
const body = ctx.request.body;
|
|
@@ -301,8 +302,10 @@ async function handleAgentCompletions(ctx, plugin) {
|
|
|
301
302
|
)
|
|
302
303
|
);
|
|
303
304
|
originalWrite((0, import_openai_format.formatSSEDone)());
|
|
305
|
+
(0, import_usage.setAiApiUsageUnavailable)(ctx, completionId);
|
|
304
306
|
ctx.state.aiApiStreamResult = { succeeded: true, id: completionId };
|
|
305
307
|
} else {
|
|
308
|
+
(0, import_usage.setAiApiUsageUnavailable)(ctx, completionId);
|
|
306
309
|
ctx.state.aiApiStreamResult = { succeeded: false, id: completionId, errorCode: "agent_error" };
|
|
307
310
|
}
|
|
308
311
|
if (!ctx.res.writableEnded && !ctx.res.destroyed) originalEnd();
|
|
@@ -342,6 +345,7 @@ async function handleAgentCompletions(ctx, plugin) {
|
|
|
342
345
|
// Clients can detect agent mode by checking usage.total_tokens === 0.
|
|
343
346
|
usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
|
|
344
347
|
};
|
|
348
|
+
(0, import_usage.setAiApiUsageUnavailable)(ctx, completionId);
|
|
345
349
|
}
|
|
346
350
|
} catch (err) {
|
|
347
351
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
@@ -51,6 +51,7 @@ async function authenticateBearer(ctx) {
|
|
|
51
51
|
}
|
|
52
52
|
try {
|
|
53
53
|
if (ctx.state.currentUser) {
|
|
54
|
+
ctx.state.aiApiAuthType = ctx.state.oauthPrincipal ? "oidc" : "bearer";
|
|
54
55
|
if (!ctx.state.currentRole) {
|
|
55
56
|
const requestedRole = ctx.get("X-Role");
|
|
56
57
|
const rolesRepository2 = ctx.db.getRepository("users.roles", ctx.state.currentUser.id);
|
|
@@ -35,6 +35,7 @@ var import_openai_format = require("../utils/openai-format");
|
|
|
35
35
|
var import_resolve_service = require("../utils/resolve-service");
|
|
36
36
|
var import_streaming = require("../utils/streaming");
|
|
37
37
|
var import_role_permission = require("../middleware/role-permission");
|
|
38
|
+
var import_usage = require("../usage");
|
|
38
39
|
async function handleChatCompletions(ctx, plugin) {
|
|
39
40
|
var _a;
|
|
40
41
|
const body = ctx.request.body;
|
|
@@ -208,18 +209,17 @@ async function handleNonStreamingCompletion(ctx, chatModel, messages, completion
|
|
|
208
209
|
const textPart = result.content.find((c) => c.type === "text");
|
|
209
210
|
content = (textPart == null ? void 0 : textPart.text) || JSON.stringify(result.content);
|
|
210
211
|
}
|
|
211
|
-
const usage = result.usage_metadata
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
} : { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 };
|
|
212
|
+
const usage = (0, import_usage.setAiApiUsageResult)(ctx, result.usage_metadata, {
|
|
213
|
+
gatewayResponseId: completionId,
|
|
214
|
+
providerRequestId: (0, import_usage.extractProviderRequestId)(result)
|
|
215
|
+
});
|
|
216
216
|
ctx.status = 200;
|
|
217
217
|
const toolCalls = normalizeToolCalls(result.tool_calls);
|
|
218
218
|
ctx.body = (0, import_openai_format.toOpenAIResponse)({
|
|
219
219
|
id: completionId,
|
|
220
220
|
model: modelName,
|
|
221
221
|
content,
|
|
222
|
-
usage,
|
|
222
|
+
usage: usage ?? { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
|
|
223
223
|
toolCalls
|
|
224
224
|
});
|
|
225
225
|
}
|
|
@@ -244,6 +244,7 @@ async function handleStreamingCompletion(ctx, chatModel, messages, completionId,
|
|
|
244
244
|
);
|
|
245
245
|
const requestAbort = (0, import_streaming.createRequestAbortController)(ctx);
|
|
246
246
|
let usage;
|
|
247
|
+
let providerRequestId;
|
|
247
248
|
let finishReason = "stop";
|
|
248
249
|
try {
|
|
249
250
|
const stream = await chatModel.stream(messages, { ...providerRequestParameters, signal: requestAbort.signal });
|
|
@@ -277,12 +278,9 @@ async function handleStreamingCompletion(ctx, chatModel, messages, completionId,
|
|
|
277
278
|
);
|
|
278
279
|
}
|
|
279
280
|
if (chunk.usage_metadata) {
|
|
280
|
-
usage =
|
|
281
|
-
prompt_tokens: chunk.usage_metadata.input_tokens || 0,
|
|
282
|
-
completion_tokens: chunk.usage_metadata.output_tokens || 0,
|
|
283
|
-
total_tokens: chunk.usage_metadata.total_tokens || 0
|
|
284
|
-
};
|
|
281
|
+
usage = (0, import_usage.normalizeUsage)(chunk.usage_metadata) ?? usage;
|
|
285
282
|
}
|
|
283
|
+
providerRequestId = providerRequestId ?? (0, import_usage.extractProviderRequestId)(chunk);
|
|
286
284
|
}
|
|
287
285
|
await (0, import_streaming.writeResponse)(
|
|
288
286
|
ctx,
|
|
@@ -296,7 +294,8 @@ async function handleStreamingCompletion(ctx, chatModel, messages, completionId,
|
|
|
296
294
|
)
|
|
297
295
|
);
|
|
298
296
|
await (0, import_streaming.writeResponse)(ctx, (0, import_openai_format.formatSSEDone)());
|
|
299
|
-
ctx
|
|
297
|
+
(0, import_usage.setAiApiUsageResult)(ctx, usage, { gatewayResponseId: completionId, providerRequestId });
|
|
298
|
+
ctx.state.aiApiStreamResult = { succeeded: true, id: completionId };
|
|
300
299
|
} catch (err) {
|
|
301
300
|
ctx.log.error("AI API streaming error:", err);
|
|
302
301
|
if (!ctx.res.destroyed && !ctx.res.writableEnded) {
|
|
@@ -310,7 +309,8 @@ async function handleStreamingCompletion(ctx, chatModel, messages, completionId,
|
|
|
310
309
|
})
|
|
311
310
|
);
|
|
312
311
|
}
|
|
313
|
-
ctx
|
|
312
|
+
(0, import_usage.setAiApiUsageResult)(ctx, usage, { gatewayResponseId: completionId, providerRequestId });
|
|
313
|
+
ctx.state.aiApiStreamResult = { succeeded: false, id: completionId, errorCode: "stream_error" };
|
|
314
314
|
} finally {
|
|
315
315
|
requestAbort.dispose();
|
|
316
316
|
if (!ctx.res.writableEnded && !ctx.res.destroyed) ctx.res.end();
|
|
@@ -32,6 +32,7 @@ module.exports = __toCommonJS(completions_exports);
|
|
|
32
32
|
var import_openai_format = require("../utils/openai-format");
|
|
33
33
|
var import_resolve_service = require("../utils/resolve-service");
|
|
34
34
|
var import_streaming = require("../utils/streaming");
|
|
35
|
+
var import_usage = require("../usage");
|
|
35
36
|
async function handleCompletions(ctx, plugin) {
|
|
36
37
|
var _a;
|
|
37
38
|
const body = ctx.request.body;
|
|
@@ -159,11 +160,10 @@ async function handleNonStreamingTextCompletion(ctx, chatModel, messages, comple
|
|
|
159
160
|
const textPart = result.content.find((c) => c.type === "text");
|
|
160
161
|
text = (textPart == null ? void 0 : textPart.text) || JSON.stringify(result.content);
|
|
161
162
|
}
|
|
162
|
-
const usage = result.usage_metadata
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
} : { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 };
|
|
163
|
+
const usage = (0, import_usage.setAiApiUsageResult)(ctx, result.usage_metadata, {
|
|
164
|
+
gatewayResponseId: completionId,
|
|
165
|
+
providerRequestId: (0, import_usage.extractProviderRequestId)(result)
|
|
166
|
+
});
|
|
167
167
|
ctx.status = 200;
|
|
168
168
|
ctx.body = {
|
|
169
169
|
id: completionId,
|
|
@@ -179,7 +179,7 @@ async function handleNonStreamingTextCompletion(ctx, chatModel, messages, comple
|
|
|
179
179
|
finish_reason: "stop"
|
|
180
180
|
}
|
|
181
181
|
],
|
|
182
|
-
usage
|
|
182
|
+
usage: usage ?? { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
|
|
183
183
|
};
|
|
184
184
|
}
|
|
185
185
|
async function handleStreamingTextCompletion(ctx, chatModel, messages, completionId, modelName) {
|
|
@@ -192,6 +192,7 @@ async function handleStreamingTextCompletion(ctx, chatModel, messages, completio
|
|
|
192
192
|
ctx.status = 200;
|
|
193
193
|
const requestAbort = (0, import_streaming.createRequestAbortController)(ctx);
|
|
194
194
|
let usage;
|
|
195
|
+
let providerRequestId;
|
|
195
196
|
try {
|
|
196
197
|
const stream = await chatModel.stream(messages, { signal: requestAbort.signal });
|
|
197
198
|
for await (const chunk of stream) {
|
|
@@ -224,12 +225,9 @@ async function handleStreamingTextCompletion(ctx, chatModel, messages, completio
|
|
|
224
225
|
);
|
|
225
226
|
}
|
|
226
227
|
if (chunk.usage_metadata) {
|
|
227
|
-
usage =
|
|
228
|
-
prompt_tokens: chunk.usage_metadata.input_tokens || 0,
|
|
229
|
-
completion_tokens: chunk.usage_metadata.output_tokens || 0,
|
|
230
|
-
total_tokens: chunk.usage_metadata.total_tokens || 0
|
|
231
|
-
};
|
|
228
|
+
usage = (0, import_usage.normalizeUsage)(chunk.usage_metadata) ?? usage;
|
|
232
229
|
}
|
|
230
|
+
providerRequestId = providerRequestId ?? (0, import_usage.extractProviderRequestId)(chunk);
|
|
233
231
|
}
|
|
234
232
|
await (0, import_streaming.writeResponse)(
|
|
235
233
|
ctx,
|
|
@@ -250,7 +248,8 @@ async function handleStreamingTextCompletion(ctx, chatModel, messages, completio
|
|
|
250
248
|
})
|
|
251
249
|
);
|
|
252
250
|
await (0, import_streaming.writeResponse)(ctx, (0, import_openai_format.formatSSEDone)());
|
|
253
|
-
ctx
|
|
251
|
+
(0, import_usage.setAiApiUsageResult)(ctx, usage, { gatewayResponseId: completionId, providerRequestId });
|
|
252
|
+
ctx.state.aiApiStreamResult = { succeeded: true, id: completionId };
|
|
254
253
|
} catch (err) {
|
|
255
254
|
ctx.log.error("AI API completions streaming error:", err);
|
|
256
255
|
if (!ctx.res.destroyed && !ctx.res.writableEnded) {
|
|
@@ -264,7 +263,8 @@ async function handleStreamingTextCompletion(ctx, chatModel, messages, completio
|
|
|
264
263
|
})
|
|
265
264
|
);
|
|
266
265
|
}
|
|
267
|
-
ctx
|
|
266
|
+
(0, import_usage.setAiApiUsageResult)(ctx, usage, { gatewayResponseId: completionId, providerRequestId });
|
|
267
|
+
ctx.state.aiApiStreamResult = { succeeded: false, id: completionId, errorCode: "stream_error" };
|
|
268
268
|
} finally {
|
|
269
269
|
requestAbort.dispose();
|
|
270
270
|
if (!ctx.res.writableEnded && !ctx.res.destroyed) ctx.res.end();
|
|
@@ -31,6 +31,7 @@ __export(embeddings_exports, {
|
|
|
31
31
|
module.exports = __toCommonJS(embeddings_exports);
|
|
32
32
|
var import_openai_format = require("../utils/openai-format");
|
|
33
33
|
var import_resolve_service = require("../utils/resolve-service");
|
|
34
|
+
var import_usage = require("../usage");
|
|
34
35
|
async function handleEmbeddings(ctx, plugin) {
|
|
35
36
|
var _a;
|
|
36
37
|
const body = ctx.request.body;
|
|
@@ -153,6 +154,7 @@ async function handleEmbeddings(ctx, plugin) {
|
|
|
153
154
|
const vectors = await embeddingModel.embedDocuments(inputs);
|
|
154
155
|
ctx.status = 200;
|
|
155
156
|
ctx.set("Content-Type", "application/json");
|
|
157
|
+
(0, import_usage.setAiApiUsageUnavailable)(ctx);
|
|
156
158
|
ctx.body = (0, import_openai_format.toOpenAIEmbeddingsResponse)({
|
|
157
159
|
model: body.model,
|
|
158
160
|
embeddings: vectors,
|
|
@@ -55,7 +55,7 @@ const API_PREFIX = "/api/ai-llm/v1";
|
|
|
55
55
|
function createAiLlmRouter(plugin) {
|
|
56
56
|
const checkRateLimit = (0, import_rate_limit.createRateLimitMiddleware)(plugin.rateLimiter);
|
|
57
57
|
return async (ctx, next) => {
|
|
58
|
-
var _a, _b
|
|
58
|
+
var _a, _b;
|
|
59
59
|
const { path, method } = ctx;
|
|
60
60
|
if (!path.startsWith(API_PREFIX)) {
|
|
61
61
|
return next();
|
|
@@ -78,7 +78,7 @@ function createAiLlmRouter(plugin) {
|
|
|
78
78
|
const rawBody = await getRawBody(ctx);
|
|
79
79
|
ctx.request.body = JSON.parse(rawBody);
|
|
80
80
|
} catch (bodyErr) {
|
|
81
|
-
const status =
|
|
81
|
+
const status = bodyErr && typeof bodyErr === "object" && "statusCode" in bodyErr && bodyErr.statusCode === 413 ? 413 : 400;
|
|
82
82
|
const message = status === 413 ? "Request body too large (max 10 MB)" : "Invalid JSON in request body";
|
|
83
83
|
ctx.status = status;
|
|
84
84
|
ctx.body = (0, import_openai_format.toOpenAIError)(status, message, "invalid_request_error");
|
|
@@ -100,12 +100,13 @@ function createAiLlmRouter(plugin) {
|
|
|
100
100
|
logRequest(ctx, requestId, "-", "rate_limited", 0);
|
|
101
101
|
return;
|
|
102
102
|
}
|
|
103
|
-
const
|
|
103
|
+
const requestBody = ctx.request.body || {};
|
|
104
|
+
const model = requestBody.model === void 0 || requestBody.model === null ? "-" : String(requestBody.model);
|
|
104
105
|
const isUsageEndpoint = method === "POST" && (subPath === "/chat/completions" || subPath === "/completions" || subPath === "/embeddings");
|
|
106
|
+
const isStreamingEndpoint = method === "POST" && (subPath === "/chat/completions" || subPath === "/completions");
|
|
105
107
|
const resolvedMode = isUsageEndpoint ? await resolveMode(ctx) : "llm";
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
if (streaming && (subPath === "/chat/completions" || subPath === "/completions")) {
|
|
108
|
+
const streaming = isStreamingEndpoint && (0, import_streaming.isStreamingRequested)(requestBody.stream);
|
|
109
|
+
if (streaming) {
|
|
109
110
|
const streamOptions = requestBody.stream_options;
|
|
110
111
|
ctx.request.body = {
|
|
111
112
|
...requestBody,
|
|
@@ -118,7 +119,7 @@ function createAiLlmRouter(plugin) {
|
|
|
118
119
|
const t0 = Date.now();
|
|
119
120
|
let usageId;
|
|
120
121
|
try {
|
|
121
|
-
usageId = isUsageEndpoint ? await (0, import_usage.startUsageRecord)(ctx, requestId, subPath,
|
|
122
|
+
usageId = isUsageEndpoint ? await (0, import_usage.startUsageRecord)(ctx, requestId, subPath, model, streaming, resolvedMode) : void 0;
|
|
122
123
|
} catch (usageError) {
|
|
123
124
|
ctx.log.error("AI API usage record could not be created:", usageError);
|
|
124
125
|
}
|
|
@@ -129,7 +130,7 @@ function createAiLlmRouter(plugin) {
|
|
|
129
130
|
ctx,
|
|
130
131
|
requestId,
|
|
131
132
|
model,
|
|
132
|
-
((
|
|
133
|
+
((_a = ctx.state.aiApiStreamResult) == null ? void 0 : _a.succeeded) === false ? "error" : "ok",
|
|
133
134
|
Date.now() - t0
|
|
134
135
|
);
|
|
135
136
|
return;
|
|
@@ -142,8 +143,8 @@ function createAiLlmRouter(plugin) {
|
|
|
142
143
|
if (method === "POST" && subPath === "/completions") {
|
|
143
144
|
const completionsMode = resolvedMode;
|
|
144
145
|
if (completionsMode === "agent") {
|
|
145
|
-
const reqBody = ctx.request.body;
|
|
146
|
-
if (
|
|
146
|
+
const reqBody = ctx.request.body || {};
|
|
147
|
+
if (reqBody.prompt !== void 0) {
|
|
147
148
|
const prompt = typeof reqBody.prompt === "string" ? reqBody.prompt : Array.isArray(reqBody.prompt) ? reqBody.prompt.join("\n") : String(reqBody.prompt);
|
|
148
149
|
ctx.request.body = { ...reqBody, messages: [{ role: "user", content: prompt }] };
|
|
149
150
|
}
|
|
@@ -155,7 +156,7 @@ function createAiLlmRouter(plugin) {
|
|
|
155
156
|
ctx,
|
|
156
157
|
requestId,
|
|
157
158
|
model,
|
|
158
|
-
((
|
|
159
|
+
((_b = ctx.state.aiApiStreamResult) == null ? void 0 : _b.succeeded) === false ? "error" : "ok",
|
|
159
160
|
Date.now() - t0
|
|
160
161
|
);
|
|
161
162
|
return;
|