plugin-ai-api 1.0.15 → 1.0.21
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 +10 -0
- package/dist/client/302.fc3a3491b4ec2dfd.js +10 -0
- package/dist/client/562.17a0a299d2e5152c.js +10 -0
- package/dist/client/757.a01403fb7a1bea01.js +10 -0
- package/dist/client/902.92e1daaf1ab16ebf.js +10 -0
- package/dist/client/97.72979a11a067a7c9.js +10 -0
- package/dist/client/index.js +1 -1
- package/dist/client-v2/302.d27fe4ea9b0b3bf5.js +10 -0
- package/dist/client-v2/562.fb2948ee6402de95.js +10 -0
- package/dist/client-v2/757.a117ce1cf7119cea.js +10 -0
- package/dist/client-v2/902.9054d990ddc223ac.js +10 -0
- package/dist/client-v2/952.94100128b7757f56.js +10 -0
- package/dist/client-v2/97.29c663318eebbd57.js +10 -0
- package/dist/client-v2/index.js +1 -1
- package/dist/constants.js +36 -0
- package/dist/externalVersion.js +9 -10
- package/dist/locale/en-US.json +105 -10
- package/dist/locale/vi-VN.json +105 -0
- package/dist/locale/zh-CN.json +105 -10
- package/dist/server/billing.js +331 -0
- package/dist/server/collections/ai-api-config.js +18 -0
- package/dist/server/collections/ai-api-model-metadata.js +83 -0
- package/dist/server/collections/ai-api-model-prices.js +55 -0
- package/dist/server/collections/ai-api-usage-records.js +9 -0
- package/dist/server/collections/ai-api-user-quota-buckets.js +54 -0
- package/dist/server/collections/ai-api-user-quota-policies.js +62 -0
- package/dist/server/plugin.js +36 -3
- package/dist/server/resource/ai-api-config.js +25 -0
- package/dist/server/resource/ai-api-usage-monitor.js +86 -0
- package/dist/server/routes/agent-completions.js +62 -51
- package/dist/server/routes/auth.js +11 -1
- package/dist/server/routes/chat-completions.js +157 -6
- package/dist/server/routes/completions.js +20 -3
- package/dist/server/routes/models.js +78 -20
- package/dist/server/routes/router.js +108 -23
- package/dist/server/usage.js +19 -2
- package/dist/server/utils/app-observability.js +110 -0
- package/dist/server/utils/streaming.js +15 -1
- package/dist/server/validation.js +120 -0
- package/dist/swagger.js +32 -1
- package/package.json +1 -1
- package/src/client/components/AiApiRolePermissions.tsx +11 -169
- package/src/client/locale.ts +11 -21
- package/src/client/plugin.tsx +82 -48
- package/src/client-v2/__tests__/settings-registration.test.tsx +58 -0
- package/src/client-v2/components/AiApiRolePermissions.tsx +173 -0
- package/src/client-v2/locale.ts +21 -0
- package/src/client-v2/pages/GeneralPage.tsx +183 -0
- package/src/client-v2/pages/ModelMetadataPage.tsx +280 -0
- package/src/client-v2/pages/ModelPricingPage.tsx +285 -0
- package/src/client-v2/pages/RolePermissionsTab.tsx +14 -0
- package/src/client-v2/pages/UsagePage.tsx +248 -0
- package/src/client-v2/pages/UserQuotasPage.tsx +258 -0
- package/src/client-v2/pages/api.ts +16 -0
- package/src/client-v2/plugin.tsx +62 -4
- package/src/constants.ts +21 -0
- package/src/locale/en-US.json +105 -10
- package/src/locale/vi-VN.json +105 -0
- package/src/locale/zh-CN.json +105 -10
- package/src/server/__tests__/app-observability.test.ts +98 -0
- package/src/server/__tests__/billing-quota.test.ts +134 -0
- package/src/server/__tests__/billing.test.ts +33 -0
- package/src/server/__tests__/models.test.ts +74 -0
- package/src/server/__tests__/request-body.test.ts +310 -0
- package/src/server/__tests__/streaming-observability.test.ts +51 -0
- package/src/server/__tests__/usage-monitor.test.ts +63 -0
- package/src/server/__tests__/usage-route.test.ts +4 -0
- package/src/server/billing.ts +387 -0
- package/src/server/collections/ai-api-config.ts +69 -51
- package/src/server/collections/ai-api-model-metadata.ts +72 -0
- package/src/server/collections/ai-api-model-prices.ts +25 -0
- package/src/server/collections/ai-api-usage-records.ts +9 -0
- package/src/server/collections/ai-api-user-quota-buckets.ts +24 -0
- package/src/server/collections/ai-api-user-quota-policies.ts +32 -0
- package/src/server/plugin.ts +47 -5
- package/src/server/resource/ai-api-config.ts +105 -74
- package/src/server/resource/ai-api-usage-monitor.ts +74 -0
- package/src/server/routes/agent-completions.ts +77 -62
- package/src/server/routes/auth.ts +14 -1
- package/src/server/routes/chat-completions.ts +275 -6
- package/src/server/routes/completions.ts +27 -4
- package/src/server/routes/models.ts +290 -195
- package/src/server/routes/router.ts +152 -27
- package/src/server/usage.ts +19 -1
- package/src/server/utils/app-observability.ts +105 -0
- package/src/server/utils/streaming.ts +13 -1
- package/src/server/validation.ts +89 -0
- package/src/swagger.ts +38 -1
- package/dist/client/778.5c452944cb747975.js +0 -10
- package/dist/client/950.83390c5f1d5a97fb.js +0 -10
- package/dist/client-v2/950.42b30b5cc9e32b8f.js +0 -10
- package/src/client/AiApiConfigPage.tsx +0 -309
|
@@ -1,195 +1,290 @@
|
|
|
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 '../utils/openai-format';
|
|
12
|
-
import type PluginAiApiServer from '../plugin';
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* GET /api/ai-llm/v1/models
|
|
16
|
-
*
|
|
17
|
-
* Lists all available models from enabled LLM services.
|
|
18
|
-
* Model IDs use the "serviceName/modelId" format (e.g. "my-openai/gpt-4o")
|
|
19
|
-
* so clients can copy-paste the ID directly into POST /v1/chat/completions
|
|
20
|
-
* without needing to configure a defaultLlmService.
|
|
21
|
-
*
|
|
22
|
-
* Backward compatibility: resolveModelString() in resolve-service.ts still
|
|
23
|
-
* accepts bare model IDs via its 3-tier fallback (defaultLlmService / single service).
|
|
24
|
-
*/
|
|
25
|
-
export async function handleListModels(ctx: Context, plugin: PluginAiApiServer) {
|
|
26
|
-
try {
|
|
27
|
-
const aiPlugin = ctx.app.pm.get('ai') as any;
|
|
28
|
-
if (!aiPlugin) {
|
|
29
|
-
ctx.status = 500;
|
|
30
|
-
ctx.body = toOpenAIError(500, 'AI plugin not available', 'server_error');
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const config = await getPluginConfig(ctx);
|
|
35
|
-
const filter: any = {};
|
|
36
|
-
|
|
37
|
-
// If whitelist is set, apply it (match by name OR title)
|
|
38
|
-
if (config?.enabledLlmServices?.length) {
|
|
39
|
-
filter.$or = [{ name: { $in: config.enabledLlmServices } }, { title: { $in: config.enabledLlmServices } }];
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const services = await ctx.db.getRepository('llmServices').find({
|
|
43
|
-
filter,
|
|
44
|
-
sort: 'sort',
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
ctx.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
ctx.
|
|
131
|
-
ctx.
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
*
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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 '../utils/openai-format';
|
|
12
|
+
import type PluginAiApiServer from '../plugin';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* GET /api/ai-llm/v1/models
|
|
16
|
+
*
|
|
17
|
+
* Lists all available models from enabled LLM services.
|
|
18
|
+
* Model IDs use the "serviceName/modelId" format (e.g. "my-openai/gpt-4o")
|
|
19
|
+
* so clients can copy-paste the ID directly into POST /v1/chat/completions
|
|
20
|
+
* without needing to configure a defaultLlmService.
|
|
21
|
+
*
|
|
22
|
+
* Backward compatibility: resolveModelString() in resolve-service.ts still
|
|
23
|
+
* accepts bare model IDs via its 3-tier fallback (defaultLlmService / single service).
|
|
24
|
+
*/
|
|
25
|
+
export async function handleListModels(ctx: Context, plugin: PluginAiApiServer) {
|
|
26
|
+
try {
|
|
27
|
+
const aiPlugin = ctx.app.pm.get('ai') as any;
|
|
28
|
+
if (!aiPlugin) {
|
|
29
|
+
ctx.status = 500;
|
|
30
|
+
ctx.body = toOpenAIError(500, 'AI plugin not available', 'server_error');
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const config = await getPluginConfig(ctx);
|
|
35
|
+
const filter: any = {};
|
|
36
|
+
|
|
37
|
+
// If whitelist is set, apply it (match by name OR title)
|
|
38
|
+
if (config?.enabledLlmServices?.length) {
|
|
39
|
+
filter.$or = [{ name: { $in: config.enabledLlmServices } }, { title: { $in: config.enabledLlmServices } }];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const services = await ctx.db.getRepository('llmServices').find({
|
|
43
|
+
filter,
|
|
44
|
+
sort: 'sort',
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const metadataMap = await loadModelMetadata(ctx);
|
|
48
|
+
const now = Math.floor(Date.now() / 1000);
|
|
49
|
+
const models: any[] = [];
|
|
50
|
+
|
|
51
|
+
for (const service of services) {
|
|
52
|
+
if (service.enabled === false) continue;
|
|
53
|
+
|
|
54
|
+
const enabledModels = resolveEnabledModels(service);
|
|
55
|
+
const serviceLabel = service.title || service.name;
|
|
56
|
+
|
|
57
|
+
for (const model of enabledModels) {
|
|
58
|
+
const fullId = `${service.name}/${model.value}`;
|
|
59
|
+
const meta = metadataMap.get(fullId);
|
|
60
|
+
// An override row with enabled=false hides the model from the catalog.
|
|
61
|
+
if (meta && meta.enabled === false) continue;
|
|
62
|
+
models.push(buildModelObject(fullId, now, serviceLabel, meta));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
ctx.status = 200;
|
|
67
|
+
ctx.body = {
|
|
68
|
+
object: 'list',
|
|
69
|
+
data: models,
|
|
70
|
+
};
|
|
71
|
+
} catch (err) {
|
|
72
|
+
ctx.log.error('AI API list models error:', err);
|
|
73
|
+
ctx.status = 500;
|
|
74
|
+
ctx.body = toOpenAIError(500, 'Failed to list models', 'server_error');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* GET /api/ai-llm/v1/models/:model
|
|
80
|
+
*
|
|
81
|
+
* Retrieve a single model by ID.
|
|
82
|
+
* Accepts both "serviceName/modelId" format (new) and bare "modelId" (backward compat).
|
|
83
|
+
*/
|
|
84
|
+
export async function handleGetModel(ctx: Context, modelId: string, plugin: PluginAiApiServer) {
|
|
85
|
+
try {
|
|
86
|
+
const config = await getPluginConfig(ctx);
|
|
87
|
+
const filter: any = {};
|
|
88
|
+
|
|
89
|
+
if (config?.enabledLlmServices?.length) {
|
|
90
|
+
filter.$or = [{ name: { $in: config.enabledLlmServices } }, { title: { $in: config.enabledLlmServices } }];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const services = await ctx.db.getRepository('llmServices').find({
|
|
94
|
+
filter,
|
|
95
|
+
sort: 'sort',
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const metadataMap = await loadModelMetadata(ctx);
|
|
99
|
+
const now = Math.floor(Date.now() / 1000);
|
|
100
|
+
let found: any = null;
|
|
101
|
+
|
|
102
|
+
for (const service of services) {
|
|
103
|
+
if (service.enabled === false) continue;
|
|
104
|
+
const enabledModels = resolveEnabledModels(service);
|
|
105
|
+
const serviceLabel = service.title || service.name;
|
|
106
|
+
|
|
107
|
+
for (const model of enabledModels) {
|
|
108
|
+
const fullId = `${service.name}/${model.value}`;
|
|
109
|
+
// Accept both new "serviceName/modelId" format AND bare model ID (backward compat)
|
|
110
|
+
if (fullId === modelId || model.value === modelId) {
|
|
111
|
+
const meta = metadataMap.get(fullId);
|
|
112
|
+
// A disabled override hides the model — treat as not found.
|
|
113
|
+
if (meta && meta.enabled === false) continue;
|
|
114
|
+
found = buildModelObject(fullId, now, serviceLabel, meta);
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (found) break;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!found) {
|
|
122
|
+
ctx.status = 404;
|
|
123
|
+
ctx.body = toOpenAIError(404, `Model '${modelId}' not found`, 'invalid_request_error', 'model_not_found');
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
ctx.status = 200;
|
|
128
|
+
ctx.body = found;
|
|
129
|
+
} catch (err) {
|
|
130
|
+
ctx.log.error('AI API get model error:', err);
|
|
131
|
+
ctx.status = 500;
|
|
132
|
+
ctx.body = toOpenAIError(500, 'Failed to retrieve model', 'server_error');
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ─── Helpers ───
|
|
137
|
+
|
|
138
|
+
export interface ModelMetadataOverride {
|
|
139
|
+
contextWindow?: number | null;
|
|
140
|
+
maxCompletionTokens?: number | null;
|
|
141
|
+
ownedByOverride?: string | null;
|
|
142
|
+
displayName?: string | null;
|
|
143
|
+
description?: string | null;
|
|
144
|
+
enabled?: boolean;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Load all model metadata overrides keyed by "serviceName/modelId".
|
|
149
|
+
* Returns an empty map if the collection is unavailable (e.g. mid-migration).
|
|
150
|
+
*/
|
|
151
|
+
async function loadModelMetadata(ctx: Context): Promise<Map<string, ModelMetadataOverride>> {
|
|
152
|
+
const map = new Map<string, ModelMetadataOverride>();
|
|
153
|
+
try {
|
|
154
|
+
const rows = await ctx.db.getRepository('aiApiModelMetadata').find();
|
|
155
|
+
for (const row of rows) {
|
|
156
|
+
const service = row.get('llmService');
|
|
157
|
+
const model = row.get('model');
|
|
158
|
+
if (!service || !model) continue;
|
|
159
|
+
map.set(`${service}/${model}`, {
|
|
160
|
+
contextWindow: row.get('contextWindow'),
|
|
161
|
+
maxCompletionTokens: row.get('maxCompletionTokens'),
|
|
162
|
+
ownedByOverride: row.get('ownedByOverride'),
|
|
163
|
+
displayName: row.get('displayName'),
|
|
164
|
+
description: row.get('description'),
|
|
165
|
+
enabled: row.get('enabled'),
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
} catch (err) {
|
|
169
|
+
ctx.log?.warn?.('AI API model metadata unavailable, skipping overrides:', err);
|
|
170
|
+
}
|
|
171
|
+
return map;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Build an OpenAI-compatible model object, merging any admin override.
|
|
176
|
+
*
|
|
177
|
+
* Context window is emitted under BOTH keys for maximum client compatibility:
|
|
178
|
+
* - `context_window` (Groq / most OpenAI-compatible gateways)
|
|
179
|
+
* - `context_length` (OpenRouter and its consumers)
|
|
180
|
+
* Both carry the same value so a client reading either key sees the override.
|
|
181
|
+
*/
|
|
182
|
+
export function buildModelObject(
|
|
183
|
+
fullId: string,
|
|
184
|
+
created: number,
|
|
185
|
+
serviceLabel: string,
|
|
186
|
+
meta?: ModelMetadataOverride,
|
|
187
|
+
): Record<string, unknown> {
|
|
188
|
+
const model: Record<string, unknown> = {
|
|
189
|
+
id: fullId,
|
|
190
|
+
object: 'model',
|
|
191
|
+
created,
|
|
192
|
+
owned_by: meta?.ownedByOverride || serviceLabel,
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const contextWindow = toPositiveInt(meta?.contextWindow);
|
|
196
|
+
if (contextWindow !== null) {
|
|
197
|
+
model.context_window = contextWindow;
|
|
198
|
+
model.context_length = contextWindow;
|
|
199
|
+
}
|
|
200
|
+
const maxCompletionTokens = toPositiveInt(meta?.maxCompletionTokens);
|
|
201
|
+
if (maxCompletionTokens !== null) {
|
|
202
|
+
model.max_completion_tokens = maxCompletionTokens;
|
|
203
|
+
}
|
|
204
|
+
if (meta?.displayName) {
|
|
205
|
+
model.display_name = meta.displayName;
|
|
206
|
+
model.name = meta.displayName;
|
|
207
|
+
}
|
|
208
|
+
if (meta?.description) {
|
|
209
|
+
model.description = meta.description;
|
|
210
|
+
}
|
|
211
|
+
// Only surface `active` when an override row exists; a plain model stays
|
|
212
|
+
// implicitly active (matching the previous response shape).
|
|
213
|
+
if (meta) {
|
|
214
|
+
model.active = meta.enabled !== false;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return model;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function toPositiveInt(value: unknown): number | null {
|
|
221
|
+
const n = Number(value);
|
|
222
|
+
return Number.isSafeInteger(n) && n > 0 ? n : null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function getPluginConfig(ctx: Context) {
|
|
226
|
+
return ctx.db.getRepository('aiApiConfig').findOne();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Resolve enabled models for a service.
|
|
231
|
+
* Mirrors the logic from plugin-ai's ai.ts listAllEnabledModels.
|
|
232
|
+
*
|
|
233
|
+
* For 'recommended' mode, we now correctly fetch the official model list
|
|
234
|
+
* from plugin-ai's shared recommended-models module.
|
|
235
|
+
*/
|
|
236
|
+
function resolveEnabledModels(service: any): { label: string; value: string }[] {
|
|
237
|
+
const raw = service.enabledModels;
|
|
238
|
+
|
|
239
|
+
// Handle new { mode, models } format
|
|
240
|
+
if (raw && typeof raw === 'object' && !Array.isArray(raw) && raw.mode) {
|
|
241
|
+
if (raw.mode === 'recommended') {
|
|
242
|
+
return getRecommendedModelsForProvider(service.provider);
|
|
243
|
+
}
|
|
244
|
+
// 'provider' or 'custom' mode with explicitly listed models
|
|
245
|
+
return (raw.models || [])
|
|
246
|
+
.filter((m: any) => m.value)
|
|
247
|
+
.map((m: any) => ({ label: m.label || m.value, value: m.value }));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Backward compat: old string[] format
|
|
251
|
+
if (Array.isArray(raw)) {
|
|
252
|
+
if (raw.length === 0) {
|
|
253
|
+
// Empty array means no explicit models — fall back to recommended
|
|
254
|
+
return getRecommendedModelsForProvider(service.provider);
|
|
255
|
+
}
|
|
256
|
+
return raw.map((id: string) => ({ label: id, value: id }));
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// null/undefined — no explicit models set, fall back to recommended
|
|
260
|
+
return getRecommendedModelsForProvider(service.provider);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Get recommended models for a provider from plugin-ai's shared module.
|
|
265
|
+
*
|
|
266
|
+
* Uses dynamic require() to avoid a hard compile-time import path dependency.
|
|
267
|
+
* plugin-ai is always available at runtime (it's a peerDependency).
|
|
268
|
+
*
|
|
269
|
+
* Resolves the built `dist/` module first — that is what ships in a packed
|
|
270
|
+
* plugin (package.json `main` points at `dist/`, and `src/` is not published).
|
|
271
|
+
* Falls back to `src/` for the monorepo dev runtime where only sources exist.
|
|
272
|
+
* Falls back to [] if neither is available or the provider has no recommendations.
|
|
273
|
+
*/
|
|
274
|
+
function getRecommendedModelsForProvider(provider: string): { label: string; value: string }[] {
|
|
275
|
+
const modulePaths = [
|
|
276
|
+
'@nocobase/plugin-ai/dist/common/recommended-models',
|
|
277
|
+
'@nocobase/plugin-ai/src/common/recommended-models',
|
|
278
|
+
];
|
|
279
|
+
for (const modulePath of modulePaths) {
|
|
280
|
+
try {
|
|
281
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
282
|
+
const { getRecommendedModels } = require(modulePath);
|
|
283
|
+
const models = getRecommendedModels(provider);
|
|
284
|
+
return Array.isArray(models) ? models : [];
|
|
285
|
+
} catch {
|
|
286
|
+
// Try the next candidate path.
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return [];
|
|
290
|
+
}
|