plugin-ai-api 1.0.21 → 1.0.24

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 (59) hide show
  1. package/dist/client/123.e6fe04c856ce6417.js +10 -0
  2. package/dist/client/902.e74518750f1e4201.js +10 -0
  3. package/dist/client/index.js +1 -1
  4. package/dist/client-v2/123.05f1f649923f93eb.js +10 -0
  5. package/dist/client-v2/902.c7c00a565085438a.js +10 -0
  6. package/dist/client-v2/index.js +1 -1
  7. package/dist/constants.js +5 -2
  8. package/dist/locale/en-US.json +15 -1
  9. package/dist/locale/vi-VN.json +15 -1
  10. package/dist/locale/zh-CN.json +15 -1
  11. package/dist/server/collections/ai-api-user-permissions.js +67 -0
  12. package/dist/server/collections/ai-api-user-quota-policies.js +2 -1
  13. package/dist/server/plugin.js +32 -0
  14. package/dist/server/resource/ai-api-user-permissions.js +75 -0
  15. package/dist/server/routes/agent-completions.js +5 -0
  16. package/dist/server/routes/chat-completions.js +52 -27
  17. package/dist/server/routes/completions.js +59 -33
  18. package/dist/server/routes/embeddings.js +6 -14
  19. package/dist/server/routes/models.js +24 -0
  20. package/dist/server/utils/direct-llm-context.js +184 -0
  21. package/dist/server/utils/openai-format.js +17 -3
  22. package/dist/server/utils/user-permissions.js +160 -0
  23. package/dist/server/validation.js +3 -0
  24. package/dist/swagger.js +4 -3
  25. package/package.json +2 -2
  26. package/src/client/__tests__/settings-registration.test.tsx +69 -0
  27. package/src/client/plugin.tsx +14 -3
  28. package/src/client-v2/__tests__/settings-registration.test.tsx +33 -4
  29. package/src/client-v2/pages/UserPermissionsPage.tsx +322 -0
  30. package/src/client-v2/pages/UserQuotasPage.tsx +18 -0
  31. package/src/client-v2/plugin.tsx +12 -3
  32. package/src/constants.ts +7 -0
  33. package/src/locale/en-US.json +15 -1
  34. package/src/locale/vi-VN.json +15 -1
  35. package/src/locale/zh-CN.json +15 -1
  36. package/src/server/__tests__/direct-llm-context.test.ts +125 -0
  37. package/src/server/__tests__/models.test.ts +44 -2
  38. package/src/server/__tests__/openai-format.test.ts +52 -1
  39. package/src/server/__tests__/permission-sync.test.ts +109 -0
  40. package/src/server/__tests__/usage-route.test.ts +265 -5
  41. package/src/server/__tests__/user-permissions-resource.test.ts +66 -0
  42. package/src/server/__tests__/user-permissions.test.ts +284 -0
  43. package/src/server/__tests__/validation.test.ts +36 -0
  44. package/src/server/collections/ai-api-user-permissions.ts +46 -0
  45. package/src/server/collections/ai-api-user-quota-policies.ts +1 -0
  46. package/src/server/plugin.ts +42 -1
  47. package/src/server/resource/ai-api-user-permissions.ts +76 -0
  48. package/src/server/routes/agent-completions.ts +7 -0
  49. package/src/server/routes/chat-completions.ts +58 -30
  50. package/src/server/routes/completions.ts +68 -34
  51. package/src/server/routes/embeddings.ts +10 -15
  52. package/src/server/routes/models.ts +28 -0
  53. package/src/server/utils/direct-llm-context.ts +216 -0
  54. package/src/server/utils/openai-format.ts +26 -0
  55. package/src/server/utils/user-permissions.ts +218 -0
  56. package/src/server/validation.ts +3 -0
  57. package/src/swagger.ts +9 -3
  58. package/dist/client/902.92e1daaf1ab16ebf.js +0 -10
  59. package/dist/client-v2/902.9054d990ddc223ac.js +0 -10
@@ -0,0 +1,216 @@
1
+ import type { Context } from '@nocobase/actions';
2
+
3
+ export type ContextOverflowBehavior = 'reject' | 'truncate';
4
+
5
+ export interface OpenAIMessage {
6
+ role: string;
7
+ content?: unknown;
8
+ tool_calls?: unknown;
9
+ tool_call_id?: string;
10
+ [key: string]: unknown;
11
+ }
12
+
13
+ interface ModelMetadata {
14
+ contextWindow: number;
15
+ maxCompletionTokens: number;
16
+ }
17
+
18
+ interface ContextPreparationOptions {
19
+ serviceName: string;
20
+ modelId: string;
21
+ messages: OpenAIMessage[];
22
+ tools?: unknown;
23
+ maxCompletionTokens?: unknown;
24
+ maxTokens?: unknown;
25
+ }
26
+
27
+ export interface PreparedDirectLlmContext {
28
+ messages: OpenAIMessage[];
29
+ estimatedInputTokens: number;
30
+ inputTokenBudget: number;
31
+ reservedOutputTokens: number;
32
+ truncated: boolean;
33
+ }
34
+
35
+ export class DirectLlmContextError extends Error {
36
+ constructor(
37
+ readonly code:
38
+ | 'context_length_exceeded'
39
+ | 'context_estimation_unsupported'
40
+ | 'max_completion_tokens_exceeds_model_limit'
41
+ | 'model_context_metadata_not_configured',
42
+ message: string,
43
+ ) {
44
+ super(message);
45
+ this.name = 'DirectLlmContextError';
46
+ }
47
+ }
48
+
49
+ function getValue<T>(
50
+ record: { get?: (key: string) => unknown; [key: string]: unknown } | null,
51
+ key: string,
52
+ ): T | undefined {
53
+ return (record?.get?.(key) as T | undefined) ?? (record?.[key] as T | undefined);
54
+ }
55
+
56
+ function positiveInteger(value: unknown): number | undefined {
57
+ const parsed = Number(value);
58
+ return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : undefined;
59
+ }
60
+
61
+ function hasImageContent(value: unknown): boolean {
62
+ return (
63
+ Array.isArray(value) &&
64
+ value.some(
65
+ (block) => typeof block === 'object' && block !== null && (block as { type?: unknown }).type === 'image_url',
66
+ )
67
+ );
68
+ }
69
+
70
+ function estimateValueTokens(value: unknown): number {
71
+ if (value === undefined) return 0;
72
+ return Math.ceil(Buffer.byteLength(JSON.stringify(value), 'utf8') / 3);
73
+ }
74
+
75
+ function estimateMessagesTokens(messages: OpenAIMessage[]): number {
76
+ return messages.reduce((total, message) => total + estimateValueTokens(message) + 4, 0);
77
+ }
78
+
79
+ function containsUnsupportedContent(messages: OpenAIMessage[]): boolean {
80
+ return messages.some((message) => hasImageContent(message.content));
81
+ }
82
+
83
+ function isInstruction(message: OpenAIMessage): boolean {
84
+ return message.role === 'system' || message.role === 'developer';
85
+ }
86
+
87
+ function splitTurns(messages: OpenAIMessage[]): { fixed: OpenAIMessage[]; turns: OpenAIMessage[][] } {
88
+ const fixed: OpenAIMessage[] = [];
89
+ const turns: OpenAIMessage[][] = [];
90
+ let currentTurn: OpenAIMessage[] | undefined;
91
+
92
+ for (const message of messages) {
93
+ if (isInstruction(message)) {
94
+ fixed.push(message);
95
+ continue;
96
+ }
97
+ if (message.role === 'user' || !currentTurn) {
98
+ currentTurn = [message];
99
+ turns.push(currentTurn);
100
+ continue;
101
+ }
102
+ currentTurn.push(message);
103
+ }
104
+
105
+ return { fixed, turns };
106
+ }
107
+
108
+ function messagesWithTurns(messages: OpenAIMessage[], turns: OpenAIMessage[][]): OpenAIMessage[] {
109
+ const retainedMessages = new Set(turns.flat());
110
+ return messages.filter((message) => isInstruction(message) || retainedMessages.has(message));
111
+ }
112
+
113
+ async function loadModelMetadata(ctx: Context, serviceName: string, modelId: string): Promise<ModelMetadata> {
114
+ const row = await ctx.db.getRepository('aiApiModelMetadata').findOne({
115
+ filter: { llmService: serviceName, model: modelId, enabled: true },
116
+ });
117
+ const contextWindow = positiveInteger(getValue<unknown>(row, 'contextWindow'));
118
+ const maxCompletionTokens = positiveInteger(getValue<unknown>(row, 'maxCompletionTokens'));
119
+ if (!contextWindow || !maxCompletionTokens) {
120
+ throw new DirectLlmContextError(
121
+ 'model_context_metadata_not_configured',
122
+ `Context metadata is not configured for '${serviceName}/${modelId}'. Configure context window and max completion tokens.`,
123
+ );
124
+ }
125
+ return { contextWindow, maxCompletionTokens };
126
+ }
127
+
128
+ async function resolveOverflowBehavior(ctx: Context): Promise<ContextOverflowBehavior> {
129
+ const userId = ctx.state.currentUser?.id;
130
+ if (userId === null || userId === undefined) return 'reject';
131
+ const policy = await ctx.db.getRepository('aiApiUserQuotaPolicies').findOne({
132
+ filter: { userId, enabled: true },
133
+ });
134
+ return getValue<unknown>(policy, 'contextOverflowBehavior') === 'truncate' ? 'truncate' : 'reject';
135
+ }
136
+
137
+ function resolveReservedOutputTokens(options: ContextPreparationOptions, metadata: ModelMetadata): number {
138
+ const requested = positiveInteger(options.maxCompletionTokens ?? options.maxTokens);
139
+ if (requested && requested > metadata.maxCompletionTokens) {
140
+ throw new DirectLlmContextError(
141
+ 'max_completion_tokens_exceeds_model_limit',
142
+ `Requested max completion tokens (${requested}) exceeds the model limit (${metadata.maxCompletionTokens}).`,
143
+ );
144
+ }
145
+ return requested ?? metadata.maxCompletionTokens;
146
+ }
147
+
148
+ export async function prepareDirectLlmContext(
149
+ ctx: Context,
150
+ options: ContextPreparationOptions,
151
+ ): Promise<PreparedDirectLlmContext> {
152
+ if (containsUnsupportedContent(options.messages)) {
153
+ throw new DirectLlmContextError(
154
+ 'context_estimation_unsupported',
155
+ 'Context enforcement does not support image_url content without a model-specific vision token estimator.',
156
+ );
157
+ }
158
+
159
+ const [metadata, behavior] = await Promise.all([
160
+ loadModelMetadata(ctx, options.serviceName, options.modelId),
161
+ resolveOverflowBehavior(ctx),
162
+ ]);
163
+ const reservedOutputTokens = resolveReservedOutputTokens(options, metadata);
164
+ const inputTokenBudget = metadata.contextWindow - reservedOutputTokens;
165
+ if (inputTokenBudget <= 0) {
166
+ throw new DirectLlmContextError(
167
+ 'context_length_exceeded',
168
+ `The model context window (${metadata.contextWindow}) leaves no input capacity after reserving ${reservedOutputTokens} output tokens.`,
169
+ );
170
+ }
171
+
172
+ const fixedOverheadTokens = estimateValueTokens(options.tools) + (options.tools === undefined ? 0 : 4);
173
+ const originalEstimate = estimateMessagesTokens(options.messages) + fixedOverheadTokens;
174
+ if (originalEstimate <= inputTokenBudget) {
175
+ return {
176
+ messages: options.messages,
177
+ estimatedInputTokens: originalEstimate,
178
+ inputTokenBudget,
179
+ reservedOutputTokens,
180
+ truncated: false,
181
+ };
182
+ }
183
+
184
+ if (behavior === 'reject') {
185
+ throw new DirectLlmContextError(
186
+ 'context_length_exceeded',
187
+ `Estimated input tokens (${originalEstimate}) exceed the allowed input budget (${inputTokenBudget}).`,
188
+ );
189
+ }
190
+
191
+ const { turns } = splitTurns(options.messages);
192
+ let remainingTurns = turns;
193
+ let messages = messagesWithTurns(options.messages, remainingTurns);
194
+ let estimatedInputTokens = estimateMessagesTokens(messages) + fixedOverheadTokens;
195
+
196
+ while (remainingTurns.length > 1 && estimatedInputTokens > inputTokenBudget) {
197
+ remainingTurns = remainingTurns.slice(1);
198
+ messages = messagesWithTurns(options.messages, remainingTurns);
199
+ estimatedInputTokens = estimateMessagesTokens(messages) + fixedOverheadTokens;
200
+ }
201
+
202
+ if (estimatedInputTokens > inputTokenBudget) {
203
+ throw new DirectLlmContextError(
204
+ 'context_length_exceeded',
205
+ `The fixed instructions, tools, and newest conversation turn require ${estimatedInputTokens} input tokens, exceeding the allowed budget (${inputTokenBudget}).`,
206
+ );
207
+ }
208
+
209
+ return {
210
+ messages,
211
+ estimatedInputTokens,
212
+ inputTokenBudget,
213
+ reservedOutputTokens,
214
+ truncated: messages.length !== options.messages.length,
215
+ };
216
+ }
@@ -97,6 +97,14 @@ export function toOpenAIResponse(options: {
97
97
 
98
98
  // ─── OpenAI Streaming chunk format ───
99
99
 
100
+ export type OpenAIUsage = {
101
+ prompt_tokens: number | null;
102
+ completion_tokens: number | null;
103
+ total_tokens: number | null;
104
+ };
105
+
106
+ export type OpenAIStreamObject = 'chat.completion.chunk' | 'text_completion';
107
+
100
108
  export function toOpenAIStreamChunk(options: {
101
109
  id: string;
102
110
  model: string;
@@ -118,6 +126,24 @@ export function toOpenAIStreamChunk(options: {
118
126
  finish_reason: finishReason,
119
127
  },
120
128
  ],
129
+ usage: null,
130
+ };
131
+ }
132
+
133
+ export function toOpenAIUsageChunk(options: {
134
+ id: string;
135
+ model: string;
136
+ usage: OpenAIUsage;
137
+ object?: OpenAIStreamObject;
138
+ }) {
139
+ const { id, model, usage, object = 'chat.completion.chunk' } = options;
140
+ return {
141
+ id,
142
+ object,
143
+ created: Math.floor(Date.now() / 1000),
144
+ model,
145
+ choices: [],
146
+ usage,
121
147
  };
122
148
  }
123
149
 
@@ -0,0 +1,218 @@
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
+ import { Context } from '@nocobase/actions';
11
+ import { toOpenAIError } from './openai-format';
12
+
13
+ const SCOPE_TTL_MS = 15_000;
14
+
15
+ interface CachedScope {
16
+ scope: AiApiAccessScope;
17
+ expiresAt: number;
18
+ }
19
+
20
+ const scopeCache = new Map<string, CachedScope>();
21
+
22
+ /**
23
+ * Resolved per-user LLM access, layered *under* the global `aiApiConfig.enabledLlmServices`
24
+ * whitelist. A user grant can only ever narrow global access, never widen it.
25
+ */
26
+ export interface AiApiAccessScope {
27
+ /** False when the user has no aiApiUserPermissions row — global config alone decides. */
28
+ hasUserRecord: boolean;
29
+ /** True when a row exists but is switched off, denying every service. */
30
+ denyAll: boolean;
31
+ /** null means "no user-level narrowing"; an empty array denies every service. */
32
+ allowedServices: string[] | null;
33
+ allowAllModels: boolean;
34
+ allowedModels: Set<string>;
35
+ /**
36
+ * True when the lookup itself failed. Distinct from hasUserRecord:false — "this user has no
37
+ * restrictions" and "we cannot tell whether this user has restrictions" must not be conflated,
38
+ * or a mid-rolling-upgrade missing table silently lifts every user's restrictions.
39
+ */
40
+ lookupFailed: boolean;
41
+ }
42
+
43
+ const NO_RECORD_SCOPE: AiApiAccessScope = {
44
+ hasUserRecord: false,
45
+ denyAll: false,
46
+ allowedServices: null,
47
+ allowAllModels: true,
48
+ allowedModels: new Set(),
49
+ lookupFailed: false,
50
+ };
51
+
52
+ const LOOKUP_FAILED_SCOPE: AiApiAccessScope = { ...NO_RECORD_SCOPE, denyAll: true, lookupFailed: true };
53
+
54
+ /**
55
+ * Invalidate cached scopes for one user across every app in this process, or all users when
56
+ * called with no argument. Keys are `${appName}:${userId}`, and the afterSave hook only knows
57
+ * the user id, so the match is on the suffix.
58
+ *
59
+ * This is per-process only: in a multi-node deployment other nodes keep serving their cached
60
+ * scope until the 15s TTL expires.
61
+ */
62
+ export function invalidateUserPermissionCache(userId?: string | number | bigint): void {
63
+ if (userId === undefined || userId === null) {
64
+ scopeCache.clear();
65
+ return;
66
+ }
67
+ const suffix = `:${userId}`;
68
+ for (const key of scopeCache.keys()) {
69
+ if (key.endsWith(suffix)) scopeCache.delete(key);
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Sequelize instances expose columns through .get() only — a plain property read returns
75
+ * undefined for most fields. Mirrors valueOf() in billing.ts so plain-object test fixtures
76
+ * work too.
77
+ */
78
+ function valueOf<T>(row: unknown, name: string): T {
79
+ if (!row) return undefined as T;
80
+ const candidate = row as { get?: (key: string) => unknown };
81
+ if (typeof candidate.get === 'function') return candidate.get(name) as T;
82
+ return (row as Record<string, unknown>)[name] as T;
83
+ }
84
+
85
+ function toStringArray(value: unknown): string[] {
86
+ if (!Array.isArray(value)) return [];
87
+ return value.filter((item): item is string => typeof item === 'string' && item.length > 0);
88
+ }
89
+
90
+ /** Build a scope from an aiApiUserPermissions row (or absence of one). */
91
+ export function buildAccessScope(row: unknown): AiApiAccessScope {
92
+ if (!row) return NO_RECORD_SCOPE;
93
+ if (valueOf<boolean>(row, 'enabled') === false) {
94
+ return { ...NO_RECORD_SCOPE, hasUserRecord: true, denyAll: true, allowedServices: [] };
95
+ }
96
+ return {
97
+ hasUserRecord: true,
98
+ denyAll: false,
99
+ allowedServices: toStringArray(valueOf(row, 'allowedLlmServices')),
100
+ allowAllModels: valueOf<boolean>(row, 'allowAllModels') !== false,
101
+ allowedModels: new Set(toStringArray(valueOf(row, 'allowedModels'))),
102
+ lookupFailed: false,
103
+ };
104
+ }
105
+
106
+ /**
107
+ * Load the current user's access scope, cached for 15s per user id — the same TTL the role
108
+ * permission cache uses. Invalidated by afterSave/afterDestroy hooks in plugin.ts.
109
+ */
110
+ export async function resolveUserAccessScope(ctx: Context): Promise<AiApiAccessScope> {
111
+ const userId = ctx.state.currentUser?.id;
112
+ if (userId === undefined || userId === null) return NO_RECORD_SCOPE;
113
+
114
+ // Sub-apps share this process but have separate databases, so user id 1 in one app is a
115
+ // different person than user id 1 in another. Without the app prefix they collide here.
116
+ const key = `${ctx.app?.name ?? 'main'}:${userId}`;
117
+ const cached = scopeCache.get(key);
118
+ if (cached && cached.expiresAt > Date.now()) return cached.scope;
119
+
120
+ let scope: AiApiAccessScope;
121
+ try {
122
+ const row = await ctx.db.getRepository('aiApiUserPermissions').findOne({ filter: { userId } });
123
+ scope = buildAccessScope(row);
124
+ } catch (err) {
125
+ // Fail closed. A failed lookup cannot be treated as "no restrictions": during a rolling
126
+ // upgrade the table may not exist yet, and that must not lift every user's restrictions.
127
+ ctx.log?.error?.('AI API user permissions lookup failed, denying access:', err);
128
+ return LOOKUP_FAILED_SCOPE;
129
+ }
130
+
131
+ scopeCache.set(key, { scope, expiresAt: Date.now() + SCOPE_TTL_MS });
132
+ return scope;
133
+ }
134
+
135
+ function matchesService(list: string[], serviceName?: string, serviceTitle?: string): boolean {
136
+ return list.some((entry) => entry === serviceName || entry === serviceTitle);
137
+ }
138
+
139
+ /**
140
+ * Effective service check: the global whitelist AND the user grant must both allow it.
141
+ *
142
+ * An empty global whitelist means "expose all services", preserving existing behaviour.
143
+ * An empty user grant means "deny everything" — the record itself is the opt-in.
144
+ */
145
+ export function isServiceAllowed(
146
+ scope: AiApiAccessScope,
147
+ globalEnabledServices: unknown,
148
+ service: { name?: string; title?: string },
149
+ ): boolean {
150
+ // denyAll is checked before hasUserRecord: a failed lookup denies without having a record.
151
+ if (scope.denyAll) return false;
152
+ const globalList = toStringArray(globalEnabledServices);
153
+ if (globalList.length && !matchesService(globalList, service.name, service.title)) return false;
154
+ if (!scope.hasUserRecord) return true;
155
+ return matchesService(scope.allowedServices ?? [], service.name, service.title);
156
+ }
157
+
158
+ /** Model-level narrowing on top of isServiceAllowed, keyed by "serviceName/modelId". */
159
+ export function isModelAllowed(scope: AiApiAccessScope, fullModelId: string): boolean {
160
+ if (scope.denyAll) return false;
161
+ if (!scope.hasUserRecord) return true;
162
+ if (scope.allowAllModels) return true;
163
+ return scope.allowedModels.has(fullModelId);
164
+ }
165
+
166
+ /**
167
+ * Gate a completion/embedding request on the caller's effective service+model access.
168
+ *
169
+ * Writes a 403 in OpenAI error shape and returns false when access is denied, so callers
170
+ * can `if (!(await enforceModelAccess(...))) return;`.
171
+ */
172
+ export async function enforceModelAccess(
173
+ ctx: Context,
174
+ globalEnabledServices: unknown,
175
+ service: { name?: string; title?: string },
176
+ modelId: string,
177
+ ): Promise<boolean> {
178
+ const scope = await resolveUserAccessScope(ctx);
179
+ const serviceLabel = service.title || service.name;
180
+
181
+ // The denial is ours, not the caller's — report it as retryable rather than as a permission
182
+ // decision, so clients back off instead of treating the grant as permanently revoked.
183
+ if (scope.lookupFailed) {
184
+ ctx.status = 503;
185
+ ctx.body = toOpenAIError(
186
+ 503,
187
+ 'Unable to verify LLM permissions for this user. Please retry shortly.',
188
+ 'service_unavailable',
189
+ 'permission_check_failed',
190
+ );
191
+ return false;
192
+ }
193
+
194
+ if (!isServiceAllowed(scope, globalEnabledServices, service)) {
195
+ ctx.status = 403;
196
+ ctx.body = toOpenAIError(
197
+ 403,
198
+ `LLM service '${serviceLabel}' is not enabled for API access`,
199
+ 'permission_denied',
200
+ 'model_not_available',
201
+ );
202
+ return false;
203
+ }
204
+
205
+ if (!isModelAllowed(scope, `${service.name}/${modelId}`)) {
206
+ ctx.status = 403;
207
+ ctx.body = toOpenAIError(
208
+ 403,
209
+ `Model '${service.name}/${modelId}' is not permitted for this user. ` +
210
+ `Use GET /v1/models to see available models.`,
211
+ 'permission_denied',
212
+ 'model_not_available',
213
+ );
214
+ return false;
215
+ }
216
+
217
+ return true;
218
+ }
@@ -76,6 +76,9 @@ export function validateQuotaPolicy(model: Model): void {
76
76
  if (!['allow', 'use_reserved'].includes(String(model.get('missingUsageBehavior')))) {
77
77
  throw new Error('missingUsageBehavior must be allow or use_reserved.');
78
78
  }
79
+ if (!['reject', 'truncate'].includes(String(model.get('contextOverflowBehavior') ?? 'reject'))) {
80
+ throw new Error('contextOverflowBehavior must be reject or truncate.');
81
+ }
79
82
  try {
80
83
  dayjs().tz(String(model.get('timezone') || 'UTC'));
81
84
  } catch {
package/src/swagger.ts CHANGED
@@ -57,7 +57,10 @@ export default {
57
57
  tags: ['ai-llm'],
58
58
  summary: 'List available models',
59
59
  description:
60
- 'Returns all LLM models available across registered services. Model IDs are formatted as `serviceName/modelId`.',
60
+ 'Returns the LLM models available to the authenticated caller across registered services. Model IDs are formatted as `serviceName/modelId`.\n\n' +
61
+ "The catalog is user-scoped: it starts from `enabledLlmServices` in the AI API configuration, then narrows to the caller's " +
62
+ '`aiApiUserPermissions` record when one exists. A user grant can only narrow the global whitelist, never widen it, so two ' +
63
+ 'users may receive different lists from the same request.',
61
64
  security: [{ BearerAuth: [] }],
62
65
  responses: {
63
66
  200: {
@@ -84,6 +87,8 @@ export default {
84
87
  get: {
85
88
  tags: ['ai-llm'],
86
89
  summary: 'Get model details',
90
+ description:
91
+ 'User-scoped in the same way as `GET /v1/models`: a model the caller is not granted is reported as not found rather than disclosed.',
87
92
  security: [{ BearerAuth: [] }],
88
93
  parameters: [
89
94
  {
@@ -103,7 +108,7 @@ export default {
103
108
  },
104
109
  },
105
110
  },
106
- 404: { description: 'Model not found' },
111
+ 404: { description: 'Model not found, or not available to this user' },
107
112
  },
108
113
  },
109
114
  },
@@ -252,7 +257,8 @@ export default {
252
257
  enabledLlmServices: {
253
258
  type: 'array',
254
259
  items: { type: 'string' },
255
- description: 'List of enabled LLM service names',
260
+ description:
261
+ 'List of enabled LLM service names. This is the outer bound for every caller; per-user `aiApiUserPermissions` records can only narrow it further.',
256
262
  },
257
263
  rateLimitPerMinute: {
258
264
  type: 'integer',
@@ -1,10 +0,0 @@
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
- "use strict";(self.webpackChunkplugin_ai_api=self.webpackChunkplugin_ai_api||[]).push([["902"],{77:function(e,t,r){r.r(t),r.d(t,{default:function(){return f}});var n=r(155),a=r.n(n),l=r(59),i=r(694),o=r(650),u=r(630);function c(e,t){(null==t||t>e.length)&&(t=e.length);for(var r=0,n=Array(t);r<t;r++)n[r]=e[r];return n}function s(e,t,r,n,a,l,i){try{var o=e[l](i),u=o.value}catch(e){r(e);return}o.done?t(u):Promise.resolve(u).then(n,a)}function m(e){return function(){var t=this,r=arguments;return new Promise(function(n,a){var l=e.apply(t,r);function i(e){s(l,n,a,i,o,"next",e)}function o(e){s(l,n,a,i,o,"throw",e)}i(void 0)})}}function d(e,t){return function(e){if(Array.isArray(e))return e}(e)||function(e,t){var r,n,a=null==e?null:"u">typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(null!=a){var l=[],i=!0,o=!1;try{for(a=a.call(e);!(i=(r=a.next()).done)&&(l.push(r.value),!t||l.length!==t);i=!0);}catch(e){o=!0,n=e}finally{try{i||null==a.return||a.return()}finally{if(o)throw n}}return l}}(e,t)||function(e,t){if(e){if("string"==typeof e)return c(e,t);var r=Object.prototype.toString.call(e).slice(8,-1);if("Object"===r&&e.constructor&&(r=e.constructor.name),"Map"===r||"Set"===r)return Array.from(r);if("Arguments"===r||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r))return c(e,t)}}(e,t)||function(){throw TypeError("Invalid attempt to destructure non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function p(e,t){var r,n,a,l={label:0,sent:function(){if(1&a[0])throw a[1];return a[1]},trys:[],ops:[]},i=Object.create(("function"==typeof Iterator?Iterator:Object).prototype),o=Object.defineProperty;return o(i,"next",{value:u(0)}),o(i,"throw",{value:u(1)}),o(i,"return",{value:u(2)}),"function"==typeof Symbol&&o(i,Symbol.iterator,{value:function(){return this}}),i;function u(o){return function(u){var c=[o,u];if(r)throw TypeError("Generator is already executing.");for(;i&&(i=0,c[0]&&(l=0)),l;)try{if(r=1,n&&(a=2&c[0]?n.return:c[0]?n.throw||((a=n.return)&&a.call(n),0):n.next)&&!(a=a.call(n,c[1])).done)return a;switch(n=0,a&&(c=[2&c[0],a.value]),c[0]){case 0:case 1:a=c;break;case 4:return l.label++,{value:c[1],done:!1};case 5:l.label++,n=c[1],c=[0];continue;case 7:c=l.ops.pop(),l.trys.pop();continue;default:if(!(a=(a=l.trys).length>0&&a[a.length-1])&&(6===c[0]||2===c[0])){l=0;continue}if(3===c[0]&&(!a||c[1]>a[0]&&c[1]<a[3])){l.label=c[1];break}if(6===c[0]&&l.label<a[1]){l.label=a[1],a=c;break}if(a&&l.label<a[2]){l.label=a[2],l.ops.push(c);break}a[2]&&l.ops.pop(),l.trys.pop();continue}c=t.call(e,l)}catch(e){c=[6,e],n=0}finally{r=a=0}if(5&c[0])throw c[1];return{value:c[0]?c[1]:void 0,done:!0}}}}function f(){var e=(0,i.useFlowContext)(),t=(0,o.k)(),r=d(l.Form.useForm(),1)[0],c=d((0,n.useState)([]),2),s=c[0],f=c[1],y=d((0,n.useState)([]),2),b=y[0],h=y[1],v=d((0,n.useState)(!1),2),g=v[0],E=v[1],w=d((0,n.useState)(!1),2),S=w[0],k=w[1],I=d((0,n.useState)(),2),U=I[0],q=I[1],F=d((0,n.useState)(!1),2),T=F[0],x=F[1],C=(0,n.useCallback)(function(){return m(function(){var t,r,n,a;return p(this,function(i){switch(i.label){case 0:E(!0),i.label=1;case 1:return i.trys.push([1,3,4,5]),[4,Promise.all([e.api.request({url:"aiApiUserQuotaPolicies:list",method:"get",params:{pageSize:200,appends:["user"],sort:"-updatedAt"}}),e.api.request({url:"users:list",method:"get",params:{pageSize:200}})])];case 2:return r=(t=d.apply(void 0,[i.sent(),2]))[0],n=t[1],f((0,u.m)(r,[])),h((0,u.m)(n,[])),[3,5];case 3:return a=i.sent(),l.message.error((0,u.g)(a)),[3,5];case 4:return E(!1),[7];case 5:return[2]}})})()},[e.api]);(0,n.useEffect)(function(){C()},[C]);var A=function(e){q(e),r.setFieldsValue(e),x(!0)},P=function(e){var t;return(null==e?void 0:e.nickname)||(null==e?void 0:e.username)||(null==e?void 0:e.email)||String(null!=(t=null==e?void 0:e.id)?t:"")},j=[{title:t("User"),key:"user",width:180,render:function(e,t){return P(t.user)||String(t.userId)}},{title:t("Period"),dataIndex:"periodType",key:"periodType",width:100},{title:t("Request limit"),dataIndex:"requestLimit",key:"requestLimit",width:130,render:function(e){return null!=e?e:t("Unlimited")}},{title:t("Token limit"),dataIndex:"totalTokenLimit",key:"totalTokenLimit",width:130,render:function(e){return null!=e?e:t("Unlimited")}},{title:t("Cost limit"),dataIndex:"costLimit",key:"costLimit",width:120,render:function(e,r){return null==e?t("Unlimited"):"".concat(e," ").concat(r.currency)}},{title:t("Timezone"),dataIndex:"timezone",key:"timezone",width:140},{title:t("Status"),dataIndex:"enabled",key:"enabled",width:100,render:function(e){return a().createElement(l.Tag,{color:e?"green":"default"},e?t("Enabled"):t("Disabled"))}},{title:t("Actions"),key:"actions",width:150,fixed:"right",render:function(r,n){return a().createElement(l.Space,{size:0},a().createElement(l.Button,{type:"link",onClick:function(){return A(n)}},t("Edit")),a().createElement(l.Popconfirm,{title:t("Delete this quota?"),onConfirm:function(){return m(function(){var r;return p(this,function(a){switch(a.label){case 0:return a.trys.push([0,3,,4]),[4,e.api.request({url:"aiApiUserQuotaPolicies:destroy/".concat(n.id),method:"post"})];case 1:return a.sent(),l.message.success(t("Deleted successfully")),[4,C()];case 2:return a.sent(),[3,4];case 3:return r=a.sent(),l.message.error((0,u.g)(r)),[3,4];case 4:return[2]}})})()}},a().createElement(l.Button,{type:"link",danger:!0},t("Delete"))))}}];return a().createElement(l.Card,{title:t("User quotas"),extra:a().createElement(l.Button,{type:"primary",onClick:function(){q(void 0),r.setFieldsValue({enabled:!0,periodType:"monthly",timezone:"UTC",currency:"USD",rejectUnpricedModel:!0,missingUsageBehavior:"use_reserved"}),x(!0)}},t("Add quota"))},a().createElement(l.Table,{rowKey:"id",columns:j,dataSource:s,loading:g,scroll:{x:1100}}),a().createElement(l.Modal,{title:U?t("Edit quota"):t("Add quota"),open:T,onCancel:function(){return x(!1)},onOk:function(){return m(function(){var n,a;return p(this,function(i){switch(i.label){case 0:return[4,r.validateFields()];case 1:n=i.sent(),k(!0),i.label=2;case 2:return i.trys.push([2,5,6,7]),[4,e.api.request({url:U?"aiApiUserQuotaPolicies:update/".concat(U.id):"aiApiUserQuotaPolicies:create",method:"post",data:n})];case 3:return i.sent(),l.message.success(t("Saved successfully")),x(!1),[4,C()];case 4:return i.sent(),[3,7];case 5:return a=i.sent(),l.message.error((0,u.g)(a)),[3,7];case 6:return k(!1),[7];case 7:return[2]}})})()},confirmLoading:S,destroyOnClose:!0},a().createElement(l.Form,{form:r,layout:"vertical",preserve:!1},a().createElement(l.Form.Item,{name:"userId",label:t("User"),rules:[{required:!0}]},a().createElement(l.Select,{disabled:!!U,showSearch:!0,optionFilterProp:"label",options:b.map(function(e){return{label:P(e),value:e.id}})})),a().createElement(l.Form.Item,{name:"periodType",label:t("Period"),rules:[{required:!0}]},a().createElement(l.Select,{options:[{label:t("Daily"),value:"daily"},{label:t("Monthly"),value:"monthly"}]})),a().createElement(l.Form.Item,{name:"timezone",label:t("Timezone"),rules:[{required:!0}]},a().createElement(l.Input,{placeholder:"UTC"})),a().createElement(l.Form.Item,{name:"requestLimit",label:t("Request limit")},a().createElement(l.InputNumber,{min:0,stringMode:!0,style:{width:"100%"}})),a().createElement(l.Form.Item,{name:"totalTokenLimit",label:t("Token limit")},a().createElement(l.InputNumber,{min:0,stringMode:!0,style:{width:"100%"}})),a().createElement(l.Form.Item,{name:"costLimit",label:t("Cost limit")},a().createElement(l.InputNumber,{min:0,stringMode:!0,style:{width:"100%"}})),a().createElement(l.Form.Item,{name:"currency",label:t("Currency"),rules:[{required:!0}]},a().createElement(l.Input,null)),a().createElement(l.Form.Item,{name:"rejectUnpricedModel",label:t("Reject unpriced models"),valuePropName:"checked"},a().createElement(l.Switch,null)),a().createElement(l.Form.Item,{name:"missingUsageBehavior",label:t("Missing usage behavior"),rules:[{required:!0}]},a().createElement(l.Select,{options:[{label:t("Use reserved estimate"),value:"use_reserved"},{label:t("Allow without token charge"),value:"allow"}]})),a().createElement(l.Form.Item,{name:"enabled",label:t("Enabled"),valuePropName:"checked"},a().createElement(l.Switch,null)))))}},650:function(e,t,r){r.d(t,{k:function(){return l}});var n=r(694),a=JSON.parse('{"UU":"plugin-ai-api"}');function l(){var e=(0,n.useFlowEngine)();return function(t){return e.context.t(t,{ns:[a.UU,"client"]})}}},630:function(e,t,r){function n(e,t){var r,n;return e&&(void 0===e?"undefined":e&&"u">typeof Symbol&&e.constructor===Symbol?"symbol":typeof e)=="object"?null!=(r=null==(n=e.data)?void 0:n.data)?r:t:t}function a(e){var t;return(null!=(t=Error)&&"u">typeof Symbol&&t[Symbol.hasInstance]?!!t[Symbol.hasInstance](e):e instanceof t)?e.message:String(e)}r.d(t,{g:function(){return a},m:function(){return n}})}}]);
@@ -1,10 +0,0 @@
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
- "use strict";(self.webpackChunkplugin_ai_api_client_v2=self.webpackChunkplugin_ai_api_client_v2||[]).push([["902"],{77:function(e,t,r){r.r(t),r.d(t,{default:function(){return f}});var n=r(155),a=r.n(n),l=r(59),i=r(694),o=r(650),u=r(630);function c(e,t){(null==t||t>e.length)&&(t=e.length);for(var r=0,n=Array(t);r<t;r++)n[r]=e[r];return n}function s(e,t,r,n,a,l,i){try{var o=e[l](i),u=o.value}catch(e){r(e);return}o.done?t(u):Promise.resolve(u).then(n,a)}function m(e){return function(){var t=this,r=arguments;return new Promise(function(n,a){var l=e.apply(t,r);function i(e){s(l,n,a,i,o,"next",e)}function o(e){s(l,n,a,i,o,"throw",e)}i(void 0)})}}function d(e,t){return function(e){if(Array.isArray(e))return e}(e)||function(e,t){var r,n,a=null==e?null:"u">typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(null!=a){var l=[],i=!0,o=!1;try{for(a=a.call(e);!(i=(r=a.next()).done)&&(l.push(r.value),!t||l.length!==t);i=!0);}catch(e){o=!0,n=e}finally{try{i||null==a.return||a.return()}finally{if(o)throw n}}return l}}(e,t)||function(e,t){if(e){if("string"==typeof e)return c(e,t);var r=Object.prototype.toString.call(e).slice(8,-1);if("Object"===r&&e.constructor&&(r=e.constructor.name),"Map"===r||"Set"===r)return Array.from(r);if("Arguments"===r||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r))return c(e,t)}}(e,t)||function(){throw TypeError("Invalid attempt to destructure non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function p(e,t){var r,n,a,l={label:0,sent:function(){if(1&a[0])throw a[1];return a[1]},trys:[],ops:[]},i=Object.create(("function"==typeof Iterator?Iterator:Object).prototype),o=Object.defineProperty;return o(i,"next",{value:u(0)}),o(i,"throw",{value:u(1)}),o(i,"return",{value:u(2)}),"function"==typeof Symbol&&o(i,Symbol.iterator,{value:function(){return this}}),i;function u(o){return function(u){var c=[o,u];if(r)throw TypeError("Generator is already executing.");for(;i&&(i=0,c[0]&&(l=0)),l;)try{if(r=1,n&&(a=2&c[0]?n.return:c[0]?n.throw||((a=n.return)&&a.call(n),0):n.next)&&!(a=a.call(n,c[1])).done)return a;switch(n=0,a&&(c=[2&c[0],a.value]),c[0]){case 0:case 1:a=c;break;case 4:return l.label++,{value:c[1],done:!1};case 5:l.label++,n=c[1],c=[0];continue;case 7:c=l.ops.pop(),l.trys.pop();continue;default:if(!(a=(a=l.trys).length>0&&a[a.length-1])&&(6===c[0]||2===c[0])){l=0;continue}if(3===c[0]&&(!a||c[1]>a[0]&&c[1]<a[3])){l.label=c[1];break}if(6===c[0]&&l.label<a[1]){l.label=a[1],a=c;break}if(a&&l.label<a[2]){l.label=a[2],l.ops.push(c);break}a[2]&&l.ops.pop(),l.trys.pop();continue}c=t.call(e,l)}catch(e){c=[6,e],n=0}finally{r=a=0}if(5&c[0])throw c[1];return{value:c[0]?c[1]:void 0,done:!0}}}}function f(){var e=(0,i.useFlowContext)(),t=(0,o.k)(),r=d(l.Form.useForm(),1)[0],c=d((0,n.useState)([]),2),s=c[0],f=c[1],y=d((0,n.useState)([]),2),b=y[0],h=y[1],v=d((0,n.useState)(!1),2),g=v[0],E=v[1],w=d((0,n.useState)(!1),2),S=w[0],k=w[1],I=d((0,n.useState)(),2),U=I[0],q=I[1],F=d((0,n.useState)(!1),2),T=F[0],x=F[1],C=(0,n.useCallback)(function(){return m(function(){var t,r,n,a;return p(this,function(i){switch(i.label){case 0:E(!0),i.label=1;case 1:return i.trys.push([1,3,4,5]),[4,Promise.all([e.api.request({url:"aiApiUserQuotaPolicies:list",method:"get",params:{pageSize:200,appends:["user"],sort:"-updatedAt"}}),e.api.request({url:"users:list",method:"get",params:{pageSize:200}})])];case 2:return r=(t=d.apply(void 0,[i.sent(),2]))[0],n=t[1],f((0,u.m)(r,[])),h((0,u.m)(n,[])),[3,5];case 3:return a=i.sent(),l.message.error((0,u.g)(a)),[3,5];case 4:return E(!1),[7];case 5:return[2]}})})()},[e.api]);(0,n.useEffect)(function(){C()},[C]);var A=function(e){q(e),r.setFieldsValue(e),x(!0)},P=function(e){var t;return(null==e?void 0:e.nickname)||(null==e?void 0:e.username)||(null==e?void 0:e.email)||String(null!=(t=null==e?void 0:e.id)?t:"")},j=[{title:t("User"),key:"user",width:180,render:function(e,t){return P(t.user)||String(t.userId)}},{title:t("Period"),dataIndex:"periodType",key:"periodType",width:100},{title:t("Request limit"),dataIndex:"requestLimit",key:"requestLimit",width:130,render:function(e){return null!=e?e:t("Unlimited")}},{title:t("Token limit"),dataIndex:"totalTokenLimit",key:"totalTokenLimit",width:130,render:function(e){return null!=e?e:t("Unlimited")}},{title:t("Cost limit"),dataIndex:"costLimit",key:"costLimit",width:120,render:function(e,r){return null==e?t("Unlimited"):"".concat(e," ").concat(r.currency)}},{title:t("Timezone"),dataIndex:"timezone",key:"timezone",width:140},{title:t("Status"),dataIndex:"enabled",key:"enabled",width:100,render:function(e){return a().createElement(l.Tag,{color:e?"green":"default"},e?t("Enabled"):t("Disabled"))}},{title:t("Actions"),key:"actions",width:150,fixed:"right",render:function(r,n){return a().createElement(l.Space,{size:0},a().createElement(l.Button,{type:"link",onClick:function(){return A(n)}},t("Edit")),a().createElement(l.Popconfirm,{title:t("Delete this quota?"),onConfirm:function(){return m(function(){var r;return p(this,function(a){switch(a.label){case 0:return a.trys.push([0,3,,4]),[4,e.api.request({url:"aiApiUserQuotaPolicies:destroy/".concat(n.id),method:"post"})];case 1:return a.sent(),l.message.success(t("Deleted successfully")),[4,C()];case 2:return a.sent(),[3,4];case 3:return r=a.sent(),l.message.error((0,u.g)(r)),[3,4];case 4:return[2]}})})()}},a().createElement(l.Button,{type:"link",danger:!0},t("Delete"))))}}];return a().createElement(l.Card,{title:t("User quotas"),extra:a().createElement(l.Button,{type:"primary",onClick:function(){q(void 0),r.setFieldsValue({enabled:!0,periodType:"monthly",timezone:"UTC",currency:"USD",rejectUnpricedModel:!0,missingUsageBehavior:"use_reserved"}),x(!0)}},t("Add quota"))},a().createElement(l.Table,{rowKey:"id",columns:j,dataSource:s,loading:g,scroll:{x:1100}}),a().createElement(l.Modal,{title:U?t("Edit quota"):t("Add quota"),open:T,onCancel:function(){return x(!1)},onOk:function(){return m(function(){var n,a;return p(this,function(i){switch(i.label){case 0:return[4,r.validateFields()];case 1:n=i.sent(),k(!0),i.label=2;case 2:return i.trys.push([2,5,6,7]),[4,e.api.request({url:U?"aiApiUserQuotaPolicies:update/".concat(U.id):"aiApiUserQuotaPolicies:create",method:"post",data:n})];case 3:return i.sent(),l.message.success(t("Saved successfully")),x(!1),[4,C()];case 4:return i.sent(),[3,7];case 5:return a=i.sent(),l.message.error((0,u.g)(a)),[3,7];case 6:return k(!1),[7];case 7:return[2]}})})()},confirmLoading:S,destroyOnClose:!0},a().createElement(l.Form,{form:r,layout:"vertical",preserve:!1},a().createElement(l.Form.Item,{name:"userId",label:t("User"),rules:[{required:!0}]},a().createElement(l.Select,{disabled:!!U,showSearch:!0,optionFilterProp:"label",options:b.map(function(e){return{label:P(e),value:e.id}})})),a().createElement(l.Form.Item,{name:"periodType",label:t("Period"),rules:[{required:!0}]},a().createElement(l.Select,{options:[{label:t("Daily"),value:"daily"},{label:t("Monthly"),value:"monthly"}]})),a().createElement(l.Form.Item,{name:"timezone",label:t("Timezone"),rules:[{required:!0}]},a().createElement(l.Input,{placeholder:"UTC"})),a().createElement(l.Form.Item,{name:"requestLimit",label:t("Request limit")},a().createElement(l.InputNumber,{min:0,stringMode:!0,style:{width:"100%"}})),a().createElement(l.Form.Item,{name:"totalTokenLimit",label:t("Token limit")},a().createElement(l.InputNumber,{min:0,stringMode:!0,style:{width:"100%"}})),a().createElement(l.Form.Item,{name:"costLimit",label:t("Cost limit")},a().createElement(l.InputNumber,{min:0,stringMode:!0,style:{width:"100%"}})),a().createElement(l.Form.Item,{name:"currency",label:t("Currency"),rules:[{required:!0}]},a().createElement(l.Input,null)),a().createElement(l.Form.Item,{name:"rejectUnpricedModel",label:t("Reject unpriced models"),valuePropName:"checked"},a().createElement(l.Switch,null)),a().createElement(l.Form.Item,{name:"missingUsageBehavior",label:t("Missing usage behavior"),rules:[{required:!0}]},a().createElement(l.Select,{options:[{label:t("Use reserved estimate"),value:"use_reserved"},{label:t("Allow without token charge"),value:"allow"}]})),a().createElement(l.Form.Item,{name:"enabled",label:t("Enabled"),valuePropName:"checked"},a().createElement(l.Switch,null)))))}},650:function(e,t,r){r.d(t,{k:function(){return l}});var n=r(694),a=JSON.parse('{"UU":"plugin-ai-api"}');function l(){var e=(0,n.useFlowEngine)();return function(t){return e.context.t(t,{ns:[a.UU,"client"]})}}},630:function(e,t,r){function n(e,t){var r,n;return e&&(void 0===e?"undefined":e&&"u">typeof Symbol&&e.constructor===Symbol?"symbol":typeof e)=="object"?null!=(r=null==(n=e.data)?void 0:n.data)?r:t:t}function a(e){var t;return(null!=(t=Error)&&"u">typeof Symbol&&t[Symbol.hasInstance]?!!t[Symbol.hasInstance](e):e instanceof t)?e.message:String(e)}r.d(t,{g:function(){return a},m:function(){return n}})}}]);