plugin-ai-api 1.1.1 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/README.md +51 -12
  2. package/dist/client/185.c47663fefaeb0e5b.js +10 -0
  3. package/dist/client/562.9012cfd1fa04303d.js +10 -0
  4. package/dist/client/685.b5b1e0a5b825d253.js +10 -0
  5. package/dist/client/index.js +1 -1
  6. package/dist/client-v2/185.b552dc91ec2371ba.js +10 -0
  7. package/dist/client-v2/562.db2984167250b1be.js +10 -0
  8. package/dist/client-v2/685.cf16e5b829e06f85.js +10 -0
  9. package/dist/client-v2/index.js +1 -1
  10. package/dist/externalVersion.js +8 -8
  11. package/dist/locale/en-US.json +175 -139
  12. package/dist/locale/vi-VN.json +40 -2
  13. package/dist/locale/zh-CN.json +40 -2
  14. package/dist/server/collections/ai-api-model-metadata.js +26 -0
  15. package/dist/server/collections/ai-api-response-records.js +101 -0
  16. package/dist/server/collections/ai-api-virtual-models.js +68 -0
  17. package/dist/server/middleware/response-record-resource.js +66 -0
  18. package/dist/server/middleware/role-permission.js +43 -18
  19. package/dist/server/migrations/20260901000000-remove-default-group-members.js +60 -0
  20. package/dist/server/migrations/20260902000000-seed-default-role-permissions.js +55 -0
  21. package/dist/server/migrations/20260903000000-seed-sample-response-records.js +170 -0
  22. package/dist/server/plugin.js +66 -16
  23. package/dist/server/routes/chat-completions.js +38 -6
  24. package/dist/server/routes/completions.js +16 -4
  25. package/dist/server/routes/embeddings.js +25 -6
  26. package/dist/server/routes/models.js +29 -0
  27. package/dist/server/routes/responses.js +530 -0
  28. package/dist/server/routes/router.js +65 -10
  29. package/dist/server/usage.js +25 -4
  30. package/dist/server/utils/direct-llm-context.js +1 -1
  31. package/dist/server/utils/resolve-service.js +24 -0
  32. package/dist/server/utils/response-store.js +138 -0
  33. package/dist/server/utils/responses-format.js +686 -0
  34. package/dist/server/utils/responses-stream.js +330 -0
  35. package/dist/server/utils/virtual-models.js +238 -0
  36. package/dist/server/validation.js +44 -2
  37. package/dist/swagger.js +137 -0
  38. package/package.json +34 -32
  39. package/src/__tests__/locale.test.ts +43 -0
  40. package/src/client/__tests__/settings-registration.test.tsx +1 -0
  41. package/src/client/plugin.tsx +9 -1
  42. package/src/client-v2/__tests__/settings-registration.test.tsx +1 -0
  43. package/src/client-v2/pages/ModelMetadataPage.tsx +44 -0
  44. package/src/client-v2/pages/ModelRoutingPage.tsx +238 -0
  45. package/src/client-v2/pages/UsageGroupsPage.tsx +75 -38
  46. package/src/client-v2/plugin.tsx +8 -0
  47. package/src/locale/en-US.json +175 -139
  48. package/src/locale/vi-VN.json +40 -2
  49. package/src/locale/zh-CN.json +40 -2
  50. package/src/server/__tests__/embeddings.test.ts +184 -0
  51. package/src/server/__tests__/models.test.ts +21 -1
  52. package/src/server/__tests__/response-record-resource.test.ts +50 -0
  53. package/src/server/__tests__/response-store-integration.test.ts +341 -0
  54. package/src/server/__tests__/response-store.test.ts +195 -0
  55. package/src/server/__tests__/responses-contract.test.ts +469 -0
  56. package/src/server/__tests__/responses-format.test.ts +299 -0
  57. package/src/server/__tests__/responses-router.test.ts +182 -0
  58. package/src/server/__tests__/responses-streaming.test.ts +368 -0
  59. package/src/server/__tests__/responses.test.ts +462 -0
  60. package/src/server/__tests__/role-permission.test.ts +139 -0
  61. package/src/server/__tests__/seed-role-permission.test.ts +88 -0
  62. package/src/server/__tests__/types/responses-sdk.types.test-d.ts +23 -0
  63. package/src/server/__tests__/usage-groups.test.ts +96 -0
  64. package/src/server/__tests__/usage-route.test.ts +1 -0
  65. package/src/server/__tests__/usage.test.ts +14 -0
  66. package/src/server/__tests__/validation.test.ts +66 -7
  67. package/src/server/__tests__/virtual-model-routing.test.ts +589 -0
  68. package/src/server/collections/ai-api-model-metadata.ts +26 -0
  69. package/src/server/collections/ai-api-response-records.ts +77 -0
  70. package/src/server/collections/ai-api-virtual-models.ts +58 -0
  71. package/src/server/middleware/response-record-resource.ts +44 -0
  72. package/src/server/middleware/role-permission.ts +69 -35
  73. package/src/server/migrations/20260901000000-remove-default-group-members.ts +56 -0
  74. package/src/server/migrations/20260902000000-seed-default-role-permissions.ts +46 -0
  75. package/src/server/migrations/20260903000000-seed-sample-response-records.ts +162 -0
  76. package/src/server/plugin.ts +84 -20
  77. package/src/server/resource/ai-api-config.ts +2 -1
  78. package/src/server/routes/agent-completions.ts +3 -0
  79. package/src/server/routes/chat-completions.ts +34 -10
  80. package/src/server/routes/completions.ts +16 -4
  81. package/src/server/routes/embeddings.ts +32 -10
  82. package/src/server/routes/models.ts +34 -0
  83. package/src/server/routes/responses.ts +640 -0
  84. package/src/server/routes/router.ts +81 -12
  85. package/src/server/services/__tests__/file-processor.test.ts +1 -0
  86. package/src/server/usage.ts +29 -2
  87. package/src/server/utils/app-observability.ts +1 -1
  88. package/src/server/utils/direct-llm-context.ts +2 -1
  89. package/src/server/utils/openai-format.ts +1 -0
  90. package/src/server/utils/resolve-service.ts +39 -1
  91. package/src/server/utils/response-store.ts +148 -0
  92. package/src/server/utils/responses-format.ts +974 -0
  93. package/src/server/utils/responses-stream.ts +384 -0
  94. package/src/server/utils/virtual-models.ts +320 -0
  95. package/src/server/validation.ts +49 -0
  96. package/src/swagger.ts +139 -0
  97. package/dist/client/562.44b16aad4718b4c7.js +0 -10
  98. package/dist/client/685.ae483e17b6b49c98.js +0 -10
  99. package/dist/client-v2/562.45d5c504433be38b.js +0 -10
  100. package/dist/client-v2/685.1030370b309b7d4b.js +0 -10
  101. package/dist/server/collections/ai-api-user-permissions.js +0 -67
  102. package/dist/server/collections/ai-api-user-quota-buckets.js +0 -54
  103. package/dist/server/collections/ai-api-user-quota-policies.js +0 -63
  104. package/dist/server/resource/ai-api-usage-groups.js +0 -168
  105. package/src/server/collections/ai-api-user-permissions.ts +0 -46
  106. package/src/server/collections/ai-api-user-quota-buckets.ts +0 -24
  107. package/src/server/collections/ai-api-user-quota-policies.ts +0 -33
  108. package/src/server/resource/ai-api-usage-groups.ts +0 -171
@@ -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
- roleName: state.currentRole || ((_b = state.currentRoles) == null ? void 0 : _b[0]) || "unknown",
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
+ });