cortico 0.1.1 → 0.1.3
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/README.md +3 -3
- package/package.json +1 -1
- package/src/boot.ts +29 -0
- package/src/bot.ts +40 -5
- package/src/core/README.md +12 -2
- package/src/core/blobs.ts +23 -2
- package/src/core/config-schema.ts +1 -1
- package/src/core/config.ts +2 -16
- package/src/core/core.ts +18 -6
- package/src/core/generation.ts +66 -9
- package/src/core/instance-lock.ts +6 -0
- package/src/core/loop.ts +62 -32
- package/src/core/secrets.ts +2 -4
- package/src/core/session.ts +3 -2
- package/src/core/timers.ts +8 -6
- package/src/core/types.ts +23 -4
- package/src/core/util.ts +15 -0
- package/src/deploy.ts +5 -4
- package/src/extensions/README.md +6 -4
- package/src/extensions/dry-mount.ts +34 -6
- package/src/extensions/manifest.ts +21 -11
- package/src/extensions.ts +147 -23
- package/src/launcher.ts +30 -22
- package/src/protocol/open-responses/context-helpers.ts +3 -1
- package/src/protocol/open-responses/context-log.ts +37 -9
- package/src/providers/README.md +44 -12
- package/src/providers/base.ts +2 -0
- package/src/providers/console/config.ts +36 -0
- package/src/providers/console/hub.ts +307 -0
- package/src/providers/console/settings.ts +30 -26
- package/src/providers/console/strings.ts +14 -0
- package/src/providers/console/types.ts +5 -0
- package/src/providers/hub-api.ts +3 -0
- package/src/providers/llamacpp/config.ts +38 -0
- package/src/providers/llamacpp/console/models-panel.ts +87 -4
- package/src/providers/llamacpp/console/runtime-panel.ts +36 -73
- package/src/providers/llamacpp/console/server.ts +47 -4
- package/src/providers/llamacpp/huggingface.ts +93 -0
- package/src/providers/llamacpp/index.ts +5 -1
- package/src/providers/llamacpp/native.ts +10 -5
- package/src/providers/llamacpp/options.ts +2 -0
- package/src/providers/llamacpp/strings.ts +42 -0
- package/src/providers/name.ts +13 -0
- package/src/providers/openai-responses-compat/config.ts +33 -0
- package/src/providers/openai-responses-compat/console/client.ts +5 -0
- package/src/providers/openai-responses-compat/console/reasoning-panel.ts +84 -0
- package/src/providers/openai-responses-compat/console/server.ts +135 -0
- package/src/providers/openai-responses-compat/index.ts +40 -12
- package/src/providers/openai-responses-compat/native.ts +15 -5
- package/src/providers/openai-responses-compat/strings.ts +62 -1
- package/src/providers/pricebook.ts +5 -5
- package/src/providers/registry.ts +24 -10
- package/src/providers/strings.ts +2 -0
- package/src/providers/transport/errors.ts +9 -0
- package/src/providers/transport/response-http.ts +7 -3
- package/src/providers/transport/responses-input.ts +59 -10
- package/src/web/README.md +15 -3
- package/src/web/auth.ts +80 -0
- package/src/web/client/console-pages/builtins/llm-settings/panel.ts +42 -35
- package/src/web/client/console-pages/builtins/llm-settings/pricing-panel.ts +22 -8
- package/src/web/client/console-pages/builtins/llm-settings/strings.ts +2 -4
- package/src/web/client/console-pages/host.ts +22 -5
- package/src/web/client/core/api.ts +5 -1
- package/src/web/client/core/router.ts +15 -0
- package/src/web/client/features/extensions/index.ts +276 -41
- package/src/web/client/features/extensions/strings.ts +84 -10
- package/src/web/client/features/feature.ts +1 -1
- package/src/web/client/features/live/diagnostics.ts +32 -0
- package/src/web/client/features/live/index.ts +27 -11
- package/src/web/client/features/live/protocol.ts +1 -0
- package/src/web/client/features/live/strings.ts +9 -3
- package/src/web/client/features/providers/detail.ts +309 -0
- package/src/web/client/features/providers/drafts.ts +23 -0
- package/src/web/client/features/providers/index.ts +179 -87
- package/src/web/client/features/providers/strings.ts +48 -17
- package/src/web/client/features/providers/types.ts +15 -0
- package/src/web/client/features/settings/general.ts +12 -0
- package/src/web/client/features/settings/strings.ts +6 -0
- package/src/web/client/main.ts +4 -0
- package/src/web/client/shell/index.ts +1 -1
- package/src/web/client/ui/fields.ts +3 -1
- package/src/web/client/ui/icons.ts +9 -1
- package/src/web/client/ui/prompt-input.tsx +17 -5
- package/src/web/client/ui/strings.ts +0 -2
- package/src/web/diagnostics.ts +133 -0
- package/src/web/public/login.html +67 -0
- package/src/web/public/styles.css +130 -18
- package/src/web/server.ts +229 -21
- package/src/web/shared/client-panel.ts +3 -0
- package/src/web/shared/console-protocol.ts +16 -1
- package/src/worlds/bilibili/README.md +1 -1
- package/src/worlds/bilibili/overlay/server.ts +4 -2
- package/src/worlds/minecraft/ADAPT.md +66 -0
- package/src/worlds/minecraft/README.md +8 -0
- package/src/worlds/minecraft/client-launch.ts +11 -2
- package/src/worlds/minecraft/client.ts +4 -0
- package/src/worlds/minecraft/mineflayer-fixes.ts +55 -1
- package/src/worlds/qq/normalize.ts +14 -0
- package/src/worlds/qq/world.ts +56 -10
- package/src/worlds/terminal/world.ts +3 -4
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Model section of one endpoint: what llama-server knows,
|
|
3
|
-
* The endpoint comes from
|
|
2
|
+
* Model section of one endpoint: what llama-server knows, a HuggingFace search that feeds the
|
|
3
|
+
* pull field, and `use` to put a served model into the model section. The endpoint comes from
|
|
4
|
+
* `ctx.scope.instance`.
|
|
4
5
|
*/
|
|
5
6
|
import type { ConsolePanel, ConsolePanelContext } from '../../../web/shared/client-panel.ts';
|
|
7
|
+
import type { HfFile, HfRepo } from '../huggingface.ts';
|
|
6
8
|
import type { ModelsState } from './server.ts';
|
|
7
9
|
import { panel } from '../strings.ts';
|
|
10
|
+
import { HITS_PER_PAGE } from '../../../web/client/features/extensions/index.ts';
|
|
8
11
|
|
|
9
12
|
const POLL_MS = 2_000;
|
|
10
13
|
|
|
@@ -22,8 +25,12 @@ export const modelsPanel: ConsolePanel = {
|
|
|
22
25
|
const card = ui.sheet({ title: S.modelsTitle });
|
|
23
26
|
const message = ui.msgline();
|
|
24
27
|
root.append(card.el, message);
|
|
25
|
-
/**
|
|
28
|
+
/** Typed text and search results survive a re-render: the poll redraws the card around them. */
|
|
26
29
|
let draft = '';
|
|
30
|
+
let query = '';
|
|
31
|
+
let repos: HfRepo[] | null = null;
|
|
32
|
+
let searchNote = '';
|
|
33
|
+
let chosen: { repo: string; files: HfFile[] } | null = null;
|
|
27
34
|
let lastSnapshot = '';
|
|
28
35
|
|
|
29
36
|
async function act(method: string, extra: Record<string, unknown> = {}): Promise<void> {
|
|
@@ -36,6 +43,76 @@ export const modelsPanel: ConsolePanel = {
|
|
|
36
43
|
await load(true);
|
|
37
44
|
}
|
|
38
45
|
|
|
46
|
+
async function search(text: string): Promise<void> {
|
|
47
|
+
const wanted = text.trim();
|
|
48
|
+
if (!wanted) return;
|
|
49
|
+
query = wanted;
|
|
50
|
+
chosen = null;
|
|
51
|
+
try {
|
|
52
|
+
// One page of results, the console's page size for search hits.
|
|
53
|
+
const result = await ctx.invoke<{ repos: HfRepo[] }>('search', [{ name, query: wanted, limit: HITS_PER_PAGE }]);
|
|
54
|
+
repos = result.repos;
|
|
55
|
+
searchNote = repos.length ? '' : S.noRepos;
|
|
56
|
+
} catch (error) {
|
|
57
|
+
repos = [];
|
|
58
|
+
searchNote = S.searchFailed(String(error));
|
|
59
|
+
}
|
|
60
|
+
await load(true);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function showFiles(repo: string): Promise<void> {
|
|
64
|
+
try {
|
|
65
|
+
chosen = { repo, files: (await ctx.invoke<{ files: HfFile[] }>('files', [{ name, repo }])).files };
|
|
66
|
+
} catch (error) {
|
|
67
|
+
message.textContent = String(error);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
await load(true);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function renderSearch(body: HTMLElement): void {
|
|
74
|
+
const bar = ui.rowbar();
|
|
75
|
+
const input = ui.input({
|
|
76
|
+
placeholder: S.searchPlaceholder,
|
|
77
|
+
value: query,
|
|
78
|
+
onCommit: (value) => void search(value),
|
|
79
|
+
});
|
|
80
|
+
input.setAttribute('aria-label', S.search);
|
|
81
|
+
bar.append(input, ui.button(S.search, { onClick: () => void search(input.value) }));
|
|
82
|
+
body.append(ui.field(S.search, bar));
|
|
83
|
+
if (repos === null) return;
|
|
84
|
+
if (searchNote) body.append(ui.msgline(searchNote, repos.length === 0 && searchNote !== S.noRepos));
|
|
85
|
+
if (repos.length) {
|
|
86
|
+
const table = ui.table({ head: [S.repo, S.downloads, S.likes, S.updated, ''] });
|
|
87
|
+
for (const repo of repos) {
|
|
88
|
+
const open = ui.button(S.showFiles, { size: 'sm', onClick: () => void showFiles(repo.id) });
|
|
89
|
+
table.addRow([
|
|
90
|
+
{ text: repo.id, cls: 'mono' },
|
|
91
|
+
ui.fmt.count(repo.downloads),
|
|
92
|
+
ui.fmt.count(repo.likes),
|
|
93
|
+
repo.updatedAt ? repo.updatedAt.slice(0, 10) : '',
|
|
94
|
+
open,
|
|
95
|
+
]);
|
|
96
|
+
}
|
|
97
|
+
body.append(table.el);
|
|
98
|
+
}
|
|
99
|
+
if (!chosen) return;
|
|
100
|
+
body.append(ui.section(S.filesOf(chosen.repo)));
|
|
101
|
+
const files = ui.table({ head: [S.file, S.size, S.tag, ''] });
|
|
102
|
+
if (chosen.files.length === 0) files.clear(S.noFiles);
|
|
103
|
+
for (const file of chosen.files) {
|
|
104
|
+
const pull = ui.button(S.pull, { size: 'sm', variant: 'primary', onClick: () => void act('pull', { model: file.pull }) });
|
|
105
|
+
pull.title = file.pull;
|
|
106
|
+
files.addRow([
|
|
107
|
+
{ text: file.name, cls: 'mono' },
|
|
108
|
+
file.bytes === null ? '' : bytes(file.bytes),
|
|
109
|
+
{ text: file.tag ?? '', cls: 'mono' },
|
|
110
|
+
pull,
|
|
111
|
+
]);
|
|
112
|
+
}
|
|
113
|
+
body.append(files.el);
|
|
114
|
+
}
|
|
115
|
+
|
|
39
116
|
function render(state: ModelsState, body: HTMLElement): void {
|
|
40
117
|
body.append(ui.kv([
|
|
41
118
|
{ k: S.cacheDir, v: state.cacheDir },
|
|
@@ -79,13 +156,18 @@ export const modelsPanel: ConsolePanel = {
|
|
|
79
156
|
});
|
|
80
157
|
status.append(progress.el);
|
|
81
158
|
}
|
|
82
|
-
const actions = ui.
|
|
159
|
+
const actions = ui.rowbar();
|
|
83
160
|
if (model.status === 'downloading')
|
|
84
161
|
actions.append(ui.button(S.cancel, { size: 'sm', onClick: () => void act('cancel', { model: model.id }) }));
|
|
85
162
|
else if (model.status === 'loaded' || model.status === 'sleeping')
|
|
86
163
|
actions.append(ui.button(S.unload, { size: 'sm', onClick: () => void act('unload', { model: model.id }) }));
|
|
87
164
|
else if (model.status === 'unloaded' || model.status === 'failed')
|
|
88
165
|
actions.append(ui.button(S.load, { size: 'sm', onClick: () => void act('load', { model: model.id }) }));
|
|
166
|
+
if (model.status !== 'downloading') {
|
|
167
|
+
const use = ui.button(S.use, { size: 'sm', onClick: () => void act('use', { model: model.id }) });
|
|
168
|
+
use.title = S.useTitle;
|
|
169
|
+
actions.append(use);
|
|
170
|
+
}
|
|
89
171
|
table.addRow([
|
|
90
172
|
{ text: model.id, cls: 'mono' },
|
|
91
173
|
status,
|
|
@@ -95,6 +177,7 @@ export const modelsPanel: ConsolePanel = {
|
|
|
95
177
|
]);
|
|
96
178
|
}
|
|
97
179
|
body.append(table.el);
|
|
180
|
+
renderSearch(body);
|
|
98
181
|
}
|
|
99
182
|
|
|
100
183
|
async function load(force = false): Promise<void> {
|
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
* The endpoint comes from `ctx.scope.instance`; edits are written on change.
|
|
4
4
|
*/
|
|
5
5
|
import type { ConsolePanel, ConsolePanelContext } from '../../../web/shared/client-panel.ts';
|
|
6
|
-
import type {
|
|
7
|
-
import
|
|
6
|
+
import type { ConfigValues } from '../../../core/config-schema.ts';
|
|
7
|
+
import { configField } from '../../../web/client/features/config/view.ts';
|
|
8
|
+
import type { RuntimePanelState } from './server.ts';
|
|
8
9
|
import { panel } from '../strings.ts';
|
|
9
10
|
|
|
10
|
-
type Row =
|
|
11
|
+
type Row = RuntimePanelState;
|
|
11
12
|
|
|
12
13
|
const POLL_MS = 2_000;
|
|
13
14
|
|
|
@@ -42,18 +43,35 @@ export const runtimePanel: ConsolePanel = {
|
|
|
42
43
|
await load(true);
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
46
|
+
async function saveConfig(groupId: string, values: ConfigValues): Promise<void> {
|
|
47
|
+
if (busy) return;
|
|
48
|
+
busy = true;
|
|
49
|
+
message.textContent = '';
|
|
50
|
+
try {
|
|
51
|
+
await ctx.setConfig(groupId, values);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
message.textContent = String(error);
|
|
54
|
+
} finally {
|
|
55
|
+
busy = false;
|
|
56
|
+
}
|
|
57
|
+
await load(true);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function renderConfig(row: Row, index: number, body: HTMLElement): void {
|
|
61
|
+
const { group, values } = row.config[index];
|
|
62
|
+
body.append(ui.section(group.schema.title, group.schema.description));
|
|
63
|
+
for (const [path, property] of Object.entries(group.schema.properties)) {
|
|
64
|
+
const field = configField(ui, property, values[path], () => {
|
|
65
|
+
if (field.read) void saveConfig(group.id, { [path]: field.read() });
|
|
66
|
+
}, ctx.signal);
|
|
67
|
+
field.node.setAttribute('aria-label', property.title);
|
|
68
|
+
// 自备运行时目录时后端由目录里的二进制决定,后端那一格禁用。
|
|
69
|
+
if (path.endsWith('.runtime.backend') && row.own) {
|
|
70
|
+
(field.node as HTMLSelectElement).disabled = true;
|
|
71
|
+
}
|
|
72
|
+
body.append(ui.field(property.title, field.node));
|
|
73
|
+
if (property.description) body.append(ui.msgline(property.description));
|
|
74
|
+
}
|
|
57
75
|
}
|
|
58
76
|
|
|
59
77
|
function renderUnmanaged(row: Row, body: HTMLElement): void {
|
|
@@ -74,42 +92,8 @@ export const runtimePanel: ConsolePanel = {
|
|
|
74
92
|
: install.phase === 'extracting' ? ui.pill(S.extracting, 'plain')
|
|
75
93
|
: ui.pill(S.absent, 'off');
|
|
76
94
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
value: row.release ?? '',
|
|
80
|
-
cls: 'mono',
|
|
81
|
-
onChange: (value) => void act('configure', { release: value }),
|
|
82
|
-
});
|
|
83
|
-
release.setAttribute('aria-label', S.release);
|
|
84
|
-
const backend = ui.select({
|
|
85
|
-
value: row.backend ?? '',
|
|
86
|
-
options: row.backendChoices,
|
|
87
|
-
onChange: (value) => void act('configure', { backend: value }),
|
|
88
|
-
});
|
|
89
|
-
backend.setAttribute('aria-label', S.backend);
|
|
90
|
-
backend.disabled = row.own;
|
|
91
|
-
const dir = ui.input({
|
|
92
|
-
value: row.own ? row.runtimeDir ?? '' : '',
|
|
93
|
-
cls: 'mono',
|
|
94
|
-
onChange: (value) => void act('configure', { runtimeDir: value }),
|
|
95
|
-
});
|
|
96
|
-
dir.setAttribute('aria-label', S.runtimeDir);
|
|
97
|
-
const dirRow = ui.rowbar();
|
|
98
|
-
dirRow.append(dir, ui.button(S.browse, {
|
|
99
|
-
size: 'sm',
|
|
100
|
-
onClick: async () => {
|
|
101
|
-
const picked = await ctx.pickPath({ kind: 'directory', title: S.runtimeDir, currentPath: dir.value });
|
|
102
|
-
if (picked) await act('configure', { runtimeDir: picked });
|
|
103
|
-
},
|
|
104
|
-
}));
|
|
105
|
-
body.append(
|
|
106
|
-
ui.field(S.release, release),
|
|
107
|
-
ui.msgline(S.releaseHint),
|
|
108
|
-
ui.field(S.backend, backend),
|
|
109
|
-
ui.field(S.runtimeDir, dirRow),
|
|
110
|
-
ui.msgline(S.runtimeDirHint),
|
|
111
|
-
ui.kv([{ k: S.installStatus, v: installPill }]),
|
|
112
|
-
);
|
|
95
|
+
renderConfig(row, 0, body);
|
|
96
|
+
body.append(ui.kv([{ k: S.installStatus, v: installPill }]));
|
|
113
97
|
if (row.smartAppControl === 1) body.append(ui.msgline(S.sacWarning, true));
|
|
114
98
|
if (install.phase === 'downloading' || install.phase === 'extracting') {
|
|
115
99
|
const progress = ui.progress({
|
|
@@ -133,28 +117,7 @@ export const runtimePanel: ConsolePanel = {
|
|
|
133
117
|
installBar.append(ui.h('span', 'grow'), ui.button(S.disable, { onClick: () => void act('disable') }));
|
|
134
118
|
body.append(installBar);
|
|
135
119
|
|
|
136
|
-
|
|
137
|
-
const launchRow = ui.rowbar();
|
|
138
|
-
launchRow.append(
|
|
139
|
-
launchNumber(row, 'contextSize', S.contextSize, 1),
|
|
140
|
-
launchNumber(row, 'nGpuLayers', S.nGpuLayers, 0),
|
|
141
|
-
launchNumber(row, 'parallel', S.parallel, 1),
|
|
142
|
-
);
|
|
143
|
-
const extraArgs = ui.input({
|
|
144
|
-
value: row.launch?.extraArgs ?? '',
|
|
145
|
-
cls: 'mono',
|
|
146
|
-
onChange: (value) => void act('configure', { launch: { extraArgs: value } }),
|
|
147
|
-
});
|
|
148
|
-
extraArgs.setAttribute('aria-label', S.extraArgs);
|
|
149
|
-
body.append(
|
|
150
|
-
launchRow,
|
|
151
|
-
ui.field(S.extraArgs, extraArgs),
|
|
152
|
-
ui.msgline(S.extraArgsHint),
|
|
153
|
-
ui.checkbox(S.autoStart, {
|
|
154
|
-
checked: row.autoStart,
|
|
155
|
-
onChange: (checked) => void act('configure', { autoStart: checked }),
|
|
156
|
-
}).el,
|
|
157
|
-
);
|
|
120
|
+
renderConfig(row, 1, body);
|
|
158
121
|
|
|
159
122
|
body.append(ui.section(S.serverSection, S.serverSectionDesc));
|
|
160
123
|
const server = row.server;
|
|
@@ -5,9 +5,14 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import type { ConsolePageContribution } from '../../../web/shared/console-protocol.ts';
|
|
7
7
|
import type { ProviderConsoleHost } from '../../console/types.ts';
|
|
8
|
+
import { connectionBlocks } from '../../console/config.ts';
|
|
8
9
|
import type { RouterCatalog, RouterModel } from '../catalog.ts';
|
|
9
10
|
import { LAUNCH_DEFAULTS, PINNED_RELEASE, backendChoices, defaultBackend, llamacppOptions, type LaunchOptions } from '../options.ts';
|
|
10
11
|
import type { LlamaRuntime } from '../runtime.ts';
|
|
12
|
+
import type { RuntimeState } from '../runtime.ts';
|
|
13
|
+
import { runtimeConfig } from '../config.ts';
|
|
14
|
+
import { listGguf, searchGguf } from '../huggingface.ts';
|
|
15
|
+
import { getByPath, type ConfigGroup, type ConfigValues } from '../../../core/config-schema.ts';
|
|
11
16
|
import { text } from '../strings.ts';
|
|
12
17
|
|
|
13
18
|
interface Control {
|
|
@@ -23,6 +28,11 @@ export interface ModelsState {
|
|
|
23
28
|
models: RouterModel[];
|
|
24
29
|
}
|
|
25
30
|
|
|
31
|
+
export type RuntimePanelState = RuntimeState & {
|
|
32
|
+
name: string;
|
|
33
|
+
config: Array<{ group: ConfigGroup; values: ConfigValues }>;
|
|
34
|
+
};
|
|
35
|
+
|
|
26
36
|
/** The launch fields the panel edits, each optional and applied over the stored values. */
|
|
27
37
|
type LaunchPatch = Partial<Record<keyof LaunchOptions, unknown>>;
|
|
28
38
|
|
|
@@ -41,16 +51,32 @@ export function llamacppConsole(host: ProviderConsoleHost): Partial<ConsolePageC
|
|
|
41
51
|
if (!found) throw new Error(S.instanceNameRequired);
|
|
42
52
|
return found.entry;
|
|
43
53
|
};
|
|
54
|
+
const blocks = connectionBlocks(host.language);
|
|
44
55
|
return {
|
|
56
|
+
config: [],
|
|
45
57
|
panels: [
|
|
46
|
-
{
|
|
47
|
-
{ id: '
|
|
58
|
+
{ ...blocks.endpoint, title: S.endpointPanel, description: S.endpointPanelDescription },
|
|
59
|
+
{ id: 'runtime', title: S.runtimePanel, description: S.runtimePanelDescription },
|
|
60
|
+
{ id: 'models', title: S.modelsPanel, description: S.modelsPanelDescription },
|
|
61
|
+
blocks.model,
|
|
62
|
+
blocks.pricing,
|
|
63
|
+
blocks.protocol,
|
|
48
64
|
],
|
|
49
65
|
invoke: async (panel, method, args) => {
|
|
50
66
|
if (panel === 'runtime') {
|
|
51
67
|
const value = body(args);
|
|
52
68
|
const name = value.name as string;
|
|
53
|
-
if (method === 'state')
|
|
69
|
+
if (method === 'state') {
|
|
70
|
+
const entry = entryOf(name);
|
|
71
|
+
const prefix = `providers.${name}.`;
|
|
72
|
+
const config = runtimeConfig(name, entry, host.language).map((group) => ({
|
|
73
|
+
group,
|
|
74
|
+
values: Object.fromEntries(Object.keys(group.schema.properties).map((path) => [
|
|
75
|
+
path, getByPath(entry as unknown as Record<string, unknown>, path.slice(prefix.length)) ?? '',
|
|
76
|
+
])) as ConfigValues,
|
|
77
|
+
}));
|
|
78
|
+
return { name, ...(await control(name).runtime.state(host.language)), config } satisfies RuntimePanelState;
|
|
79
|
+
}
|
|
54
80
|
if (method === 'enable') {
|
|
55
81
|
const entry = entryOf(name);
|
|
56
82
|
const options = llamacppOptions(entry);
|
|
@@ -66,7 +92,7 @@ export function llamacppConsole(host: ProviderConsoleHost): Partial<ConsolePageC
|
|
|
66
92
|
return { ok: true };
|
|
67
93
|
}
|
|
68
94
|
if (method === 'disable') {
|
|
69
|
-
await control(name).runtime.stop(host.language);
|
|
95
|
+
if (!host.editing) await control(name).runtime.stop(host.language);
|
|
70
96
|
const entry = entryOf(name);
|
|
71
97
|
const { runtime: _runtime, launch: _launch, ...rest } = entry.options ?? {};
|
|
72
98
|
host.save(name, { ...entry, options: rest });
|
|
@@ -96,6 +122,7 @@ export function llamacppConsole(host: ProviderConsoleHost): Partial<ConsolePageC
|
|
|
96
122
|
});
|
|
97
123
|
return { ok: true };
|
|
98
124
|
}
|
|
125
|
+
if (host.editing) throw new Error(host.language === 'zh' ? '请先保存配置,再执行运行时操作。' : 'Save configuration before runtime operations.');
|
|
99
126
|
if (method === 'install') {
|
|
100
127
|
await control(name).runtime.install(host.language);
|
|
101
128
|
return { ok: true };
|
|
@@ -120,6 +147,22 @@ export function llamacppConsole(host: ProviderConsoleHost): Partial<ConsolePageC
|
|
|
120
147
|
await catalog.list(true);
|
|
121
148
|
return { ok: true };
|
|
122
149
|
}
|
|
150
|
+
if (method === 'search') {
|
|
151
|
+
if (typeof value.query !== 'string' || !value.query.trim()) throw new Error(S.queryRequired);
|
|
152
|
+
if (!Number.isInteger(value.limit) || (value.limit as number) < 1) throw new Error(S.limitRequired);
|
|
153
|
+
return { repos: await searchGguf(value.query.trim(), value.limit as number) };
|
|
154
|
+
}
|
|
155
|
+
if (method === 'files') {
|
|
156
|
+
if (typeof value.repo !== 'string' || !value.repo.trim()) throw new Error(S.repoRequired);
|
|
157
|
+
return { files: await listGguf(value.repo.trim()) };
|
|
158
|
+
}
|
|
159
|
+
if (method === 'use') {
|
|
160
|
+
if (typeof value.model !== 'string' || !value.model.trim()) throw new Error(S.modelIdRequired);
|
|
161
|
+
const entry = entryOf(name);
|
|
162
|
+
host.save(name, { ...entry, spec: { thinking: false, ...entry.spec, model: value.model.trim() } });
|
|
163
|
+
return { ok: true };
|
|
164
|
+
}
|
|
165
|
+
if (host.editing) throw new Error(host.language === 'zh' ? '请先保存配置,再操作模型。' : 'Save configuration before model operations.');
|
|
123
166
|
if (typeof value.model !== 'string' || !value.model.trim()) throw new Error(S.modelIdRequired);
|
|
124
167
|
const model = value.model.trim();
|
|
125
168
|
if (method === 'pull') await catalog.download(model);
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HuggingFace lookups behind the pull field: repositories tagged `gguf`, and the GGUF files of
|
|
3
|
+
* one repository as `repo:tag` pull targets. The host is the one llama-server itself pulls from:
|
|
4
|
+
* `MODEL_ENDPOINT`, else `HF_ENDPOINT`, else huggingface.co.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface HfRepo {
|
|
8
|
+
id: string;
|
|
9
|
+
downloads: number;
|
|
10
|
+
likes: number;
|
|
11
|
+
/** ISO timestamp of the last commit; null when the listing omits it. */
|
|
12
|
+
updatedAt: string | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface HfFile {
|
|
16
|
+
name: string;
|
|
17
|
+
/** Sum over shards; null when the listing carries no sizes. */
|
|
18
|
+
bytes: number | null;
|
|
19
|
+
/** Quantization tag read from the file name; null when the name carries none. */
|
|
20
|
+
tag: string | null;
|
|
21
|
+
/** What `POST /models` takes to pull this file: `repo:tag`, or the repository alone. */
|
|
22
|
+
pull: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface HfOptions {
|
|
26
|
+
fetchImpl?: typeof fetch;
|
|
27
|
+
env?: NodeJS.ProcessEnv;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const DEFAULT_ENDPOINT = 'https://huggingface.co';
|
|
31
|
+
const SHARD_RE = /-(\d{5})-of-\d{5}\.gguf$/i;
|
|
32
|
+
const QUANT_RE = /^(?:i?q\d[a-z0-9_]*|tq\d[a-z0-9_]*|bf16|f16|f32|f64|mxfp4[a-z0-9_]*)$/i;
|
|
33
|
+
|
|
34
|
+
export function hfEndpoint(env: NodeJS.ProcessEnv = process.env): string {
|
|
35
|
+
return (env.MODEL_ENDPOINT || env.HF_ENDPOINT || DEFAULT_ENDPOINT).replace(/\/+$/, '');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function getJson(url: string, fetchImpl: typeof fetch): Promise<unknown> {
|
|
39
|
+
const res = await fetchImpl(url, { headers: { Accept: 'application/json' } });
|
|
40
|
+
if (!res.ok) throw new Error(`GET ${url} ${res.status}`);
|
|
41
|
+
return res.json();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The `limit` most-downloaded matches; without one the listing returns up to a thousand rows. */
|
|
45
|
+
export async function searchGguf(query: string, limit: number, options: HfOptions = {}): Promise<HfRepo[]> {
|
|
46
|
+
const params = new URLSearchParams({ search: query, filter: 'gguf', sort: 'downloads', direction: '-1', limit: String(limit) });
|
|
47
|
+
// The plain listing omits lastModified; `expand` names every field wanted back.
|
|
48
|
+
for (const field of ['downloads', 'likes', 'lastModified']) params.append('expand[]', field);
|
|
49
|
+
const rows = await getJson(`${hfEndpoint(options.env)}/api/models?${params}`, options.fetchImpl ?? fetch);
|
|
50
|
+
if (!Array.isArray(rows)) throw new Error('HuggingFace search returned no list');
|
|
51
|
+
const repos: HfRepo[] = [];
|
|
52
|
+
for (const row of rows as Array<Record<string, unknown>>) {
|
|
53
|
+
const id = typeof row.id === 'string' ? row.id : typeof row.modelId === 'string' ? row.modelId : null;
|
|
54
|
+
if (!id) continue;
|
|
55
|
+
repos.push({
|
|
56
|
+
id,
|
|
57
|
+
downloads: typeof row.downloads === 'number' ? row.downloads : 0,
|
|
58
|
+
likes: typeof row.likes === 'number' ? row.likes : 0,
|
|
59
|
+
updatedAt: typeof row.lastModified === 'string' ? row.lastModified : null,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
return repos;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The quantization tag in a GGUF file name: the last `-` or `.` separated token that names one, with an `UD-` prefix kept. */
|
|
66
|
+
export function quantTag(filename: string): string | null {
|
|
67
|
+
const stem = filename.replace(SHARD_RE, '.gguf').replace(/\.gguf$/i, '');
|
|
68
|
+
const tokens = stem.split(/[-.]/);
|
|
69
|
+
for (let i = tokens.length - 1; i >= 0; i--) {
|
|
70
|
+
if (!QUANT_RE.test(tokens[i])) continue;
|
|
71
|
+
return (tokens[i - 1]?.toUpperCase() === 'UD' ? 'UD-' : '') + tokens[i];
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** GGUF files of a repository, shards folded into one entry, projector files (`mmproj-*`) left out. */
|
|
77
|
+
export async function listGguf(repo: string, options: HfOptions = {}): Promise<HfFile[]> {
|
|
78
|
+
const info = await getJson(`${hfEndpoint(options.env)}/api/models/${repo.split('/').map(encodeURIComponent).join('/')}?blobs=true`, options.fetchImpl ?? fetch) as { siblings?: unknown };
|
|
79
|
+
if (!Array.isArray(info.siblings)) throw new Error('HuggingFace repository listing carries no files');
|
|
80
|
+
const files = new Map<string, HfFile>();
|
|
81
|
+
for (const sibling of info.siblings as Array<Record<string, unknown>>) {
|
|
82
|
+
const path = typeof sibling.rfilename === 'string' ? sibling.rfilename : '';
|
|
83
|
+
if (!/\.gguf$/i.test(path) || /(^|\/)mmproj/i.test(path)) continue;
|
|
84
|
+
const name = path.replace(SHARD_RE, '.gguf');
|
|
85
|
+
const lfs = sibling.lfs as { size?: unknown } | undefined;
|
|
86
|
+
const size = typeof sibling.size === 'number' ? sibling.size : typeof lfs?.size === 'number' ? lfs.size : null;
|
|
87
|
+
const entry = files.get(name);
|
|
88
|
+
if (entry) { if (size !== null && entry.bytes !== null) entry.bytes += size; continue; }
|
|
89
|
+
const tag = quantTag(name);
|
|
90
|
+
files.set(name, { name, bytes: size, tag, pull: tag ? `${repo}:${tag}` : repo });
|
|
91
|
+
}
|
|
92
|
+
return [...files.values()];
|
|
93
|
+
}
|
|
@@ -4,6 +4,7 @@ import type { Language } from '../../core/language.ts';
|
|
|
4
4
|
import { isContextOverflow } from '../transport/errors.ts';
|
|
5
5
|
import { modelsRoot, runtimesRoot } from '../../paths.ts';
|
|
6
6
|
import { RouterCatalog } from './catalog.ts';
|
|
7
|
+
import { runtimeConfig } from './config.ts';
|
|
7
8
|
import { llamacppConsole } from './console/server.ts';
|
|
8
9
|
import { LlamaCppProvider } from './native.ts';
|
|
9
10
|
import { backendChoices, llamacppOptions, normalizeLlamaCpp } from './options.ts';
|
|
@@ -23,12 +24,14 @@ const reasoningTiers = (language: Language): ReasoningTier[] => {
|
|
|
23
24
|
export default {
|
|
24
25
|
id: 'llamacpp',
|
|
25
26
|
title: 'llama.cpp',
|
|
27
|
+
description: 'Connect to or manage local llama.cpp models.',
|
|
26
28
|
defaultBaseUrl: 'http://127.0.0.1:8090/v1',
|
|
27
29
|
baseUrlSuggestions: ['http://127.0.0.1:8090/v1', 'http://127.0.0.1:8080/v1'],
|
|
28
30
|
normalize: normalizeLlamaCpp,
|
|
29
31
|
console: llamacppConsole,
|
|
32
|
+
config: runtimeConfig,
|
|
30
33
|
reasoningTiers: reasoningTiers('zh'),
|
|
31
|
-
localize: (language) => ({ reasoningTiers: reasoningTiers(language) }),
|
|
34
|
+
localize: (language) => ({ description: language === 'zh' ? '连接或托管本地 llama.cpp 模型。' : 'Connect to or manage local llama.cpp models.', reasoningTiers: reasoningTiers(language) }),
|
|
32
35
|
serviceTiers: [],
|
|
33
36
|
validateEntry: (entry, language) => {
|
|
34
37
|
const S = text(language);
|
|
@@ -87,6 +90,7 @@ export default {
|
|
|
87
90
|
apiKey,
|
|
88
91
|
log: host.log,
|
|
89
92
|
media: { enabled: () => entry.multimodal === true, read: host.readBlob },
|
|
93
|
+
keepThinking: host.keepThinking,
|
|
90
94
|
}),
|
|
91
95
|
};
|
|
92
96
|
},
|
|
@@ -2,23 +2,26 @@
|
|
|
2
2
|
* Chat Completions against llama-server. Thinking is the template's `enable_thinking` switch
|
|
3
3
|
* sent per request through `chat_template_kwargs`; templates without that variable ignore it.
|
|
4
4
|
* The server returns the chain of thought as `reasoning_content` (`--reasoning-format deepseek`),
|
|
5
|
-
* which the shared Chat assembly already maps;
|
|
5
|
+
* which the shared Chat assembly already maps; history sends it back on every request.
|
|
6
|
+
* `keepPastThinking` off blanks every turn but the synthetic opening; thinking off sends none.
|
|
6
7
|
*/
|
|
7
8
|
import type { NativeChatMessage } from '../transport/native-types.ts';
|
|
8
9
|
import type { ModelSpec, ToolSchema, Logger } from '../../core/types.ts';
|
|
9
10
|
import { nullLogger } from '../../core/util.ts';
|
|
10
11
|
import { OpenAIHttpClient } from '../transport/chat.ts';
|
|
11
|
-
import { mapTools, renderMessagesWithMedia, type CompatMediaOptions } from '../transport/history.ts';
|
|
12
|
+
import { dropPastThinking, mapTools, renderMessagesWithMedia, type CompatMediaOptions } from '../transport/history.ts';
|
|
12
13
|
|
|
13
14
|
export function buildLlamaCppRequestBody(
|
|
14
15
|
spec: ModelSpec,
|
|
15
16
|
messages: NativeChatMessage[],
|
|
16
17
|
tools?: ToolSchema[],
|
|
17
18
|
media?: CompatMediaOptions,
|
|
19
|
+
opts?: { keepThinking?: boolean },
|
|
18
20
|
): Record<string, unknown> {
|
|
21
|
+
const history = opts?.keepThinking === false ? dropPastThinking(messages) : messages;
|
|
19
22
|
const body: Record<string, unknown> = {
|
|
20
23
|
model: spec.model,
|
|
21
|
-
messages: renderMessagesWithMedia(
|
|
24
|
+
messages: renderMessagesWithMedia(history, media, { keepReasoning: spec.thinking }),
|
|
22
25
|
chat_template_kwargs: { enable_thinking: spec.thinking },
|
|
23
26
|
};
|
|
24
27
|
if (spec.thinking && spec.reasoningEffort) body.reasoning_effort = spec.reasoningEffort;
|
|
@@ -32,15 +35,17 @@ export function buildLlamaCppRequestBody(
|
|
|
32
35
|
export class LlamaCppProvider extends OpenAIHttpClient {
|
|
33
36
|
private readonly apiKey?: string;
|
|
34
37
|
private readonly media?: CompatMediaOptions;
|
|
38
|
+
private readonly keepThinking?: () => boolean;
|
|
35
39
|
|
|
36
|
-
constructor(opts: { baseUrl: string; apiKey?: string; log?: Logger; media?: CompatMediaOptions }) {
|
|
40
|
+
constructor(opts: { baseUrl: string; apiKey?: string; log?: Logger; media?: CompatMediaOptions; keepThinking?: () => boolean }) {
|
|
37
41
|
super(opts.baseUrl, opts.log ?? nullLogger());
|
|
38
42
|
this.apiKey = opts.apiKey;
|
|
39
43
|
this.media = opts.media;
|
|
44
|
+
this.keepThinking = opts.keepThinking;
|
|
40
45
|
}
|
|
41
46
|
|
|
42
47
|
protected buildBody(spec: ModelSpec, messages: NativeChatMessage[], tools?: ToolSchema[]): Record<string, unknown> {
|
|
43
|
-
return buildLlamaCppRequestBody(spec, messages, tools, this.media);
|
|
48
|
+
return buildLlamaCppRequestBody(spec, messages, tools, this.media, { keepThinking: this.keepThinking?.() });
|
|
44
49
|
}
|
|
45
50
|
|
|
46
51
|
protected headers(): Record<string, string> {
|
|
@@ -42,6 +42,8 @@ export function normalizeLlamaCpp(entry: LLMProviderEntry): LLMProviderEntry {
|
|
|
42
42
|
if (options.runtime && typeof options.runtime === 'object') {
|
|
43
43
|
const runtime = options.runtime as Partial<RuntimeOptions>;
|
|
44
44
|
options.runtime = { ...runtime, release: runtime.release ?? PINNED_RELEASE, backend: runtime.backend ?? defaultBackend() };
|
|
45
|
+
if (typeof options.runtime.release === 'string') options.runtime.release = options.runtime.release.trim();
|
|
46
|
+
if (typeof options.runtime.runtimeDir === 'string') options.runtime.runtimeDir = options.runtime.runtimeDir.trim();
|
|
45
47
|
if (options.runtime.runtimeDir === '') delete options.runtime.runtimeDir;
|
|
46
48
|
options.launch = { ...LAUNCH_DEFAULTS, ...(options.launch ?? {}) };
|
|
47
49
|
}
|
|
@@ -14,6 +14,8 @@ const zh = {
|
|
|
14
14
|
notManaged: '这条端点没有开启托管',
|
|
15
15
|
runtimeNotInstalled: '运行时还没装好',
|
|
16
16
|
runtimeUnsupported: '本机平台没有这个后端的官方构建,填自备运行时目录',
|
|
17
|
+
endpointPanel: '服务地址',
|
|
18
|
+
endpointPanelDescription: 'llama-server 监听的地址;托管的进程按它启动。',
|
|
17
19
|
runtimePanel: '运行时',
|
|
18
20
|
runtimePanelDescription: 'llama.cpp 官方二进制的下载、安装与 llama-server 进程的启停。',
|
|
19
21
|
modelsPanel: '模型',
|
|
@@ -22,6 +24,9 @@ const zh = {
|
|
|
22
24
|
unknownMethod: '未知操作',
|
|
23
25
|
instanceNameRequired: '需要端点名',
|
|
24
26
|
modelIdRequired: '需要模型 id',
|
|
27
|
+
queryRequired: '需要搜索词',
|
|
28
|
+
limitRequired: '需要结果条数',
|
|
29
|
+
repoRequired: '需要仓库名',
|
|
25
30
|
bodyRequired: '需要请求体',
|
|
26
31
|
serverUnreachable: "llama-server 不可达;请检查端点地址与服务状态。",
|
|
27
32
|
externalServer: '端点已有服务在跑(外部启动),不接管',
|
|
@@ -51,6 +56,8 @@ const en: typeof zh = {
|
|
|
51
56
|
notManaged: 'This endpoint is not managed',
|
|
52
57
|
runtimeNotInstalled: 'The runtime is not installed yet',
|
|
53
58
|
runtimeUnsupported: 'No official build for this backend on this platform; set an own runtime directory',
|
|
59
|
+
endpointPanel: 'Server address',
|
|
60
|
+
endpointPanelDescription: 'The address llama-server listens on; a hosted process starts on it.',
|
|
54
61
|
runtimePanel: 'Runtime',
|
|
55
62
|
runtimePanelDescription: 'Download and install the official llama.cpp binaries; start and stop the llama-server process.',
|
|
56
63
|
modelsPanel: 'Models',
|
|
@@ -59,6 +66,9 @@ const en: typeof zh = {
|
|
|
59
66
|
unknownMethod: 'Unknown action',
|
|
60
67
|
instanceNameRequired: 'An endpoint name is required',
|
|
61
68
|
modelIdRequired: 'A model id is required',
|
|
69
|
+
queryRequired: 'A search query is required',
|
|
70
|
+
limitRequired: 'A result count is required',
|
|
71
|
+
repoRequired: 'A repository is required',
|
|
62
72
|
bodyRequired: 'A request body is required',
|
|
63
73
|
serverUnreachable: "llama-server is unreachable; check the endpoint URL and server status.",
|
|
64
74
|
externalServer: 'A server is already running at the endpoint (started externally); not taking over',
|
|
@@ -139,6 +149,22 @@ const panelZh = {
|
|
|
139
149
|
reload: '重扫',
|
|
140
150
|
pull: '拉取',
|
|
141
151
|
pullPlaceholder: 'HuggingFace 仓库,如 ggml-org/Qwen2.5-Coder-7B-Instruct-GGUF',
|
|
152
|
+
use: '选用',
|
|
153
|
+
useTitle: '把这个模型填进「模型与生成」',
|
|
154
|
+
search: '搜索',
|
|
155
|
+
searchPlaceholder: '搜 HuggingFace 上的 GGUF 仓库,如 qwen3',
|
|
156
|
+
searchFailed: (reason: string) => `搜索失败: ${reason}`,
|
|
157
|
+
noRepos: '没有匹配的仓库。',
|
|
158
|
+
repo: '仓库',
|
|
159
|
+
downloads: '下载',
|
|
160
|
+
likes: '点赞',
|
|
161
|
+
updated: '更新',
|
|
162
|
+
showFiles: '看文件',
|
|
163
|
+
filesOf: (repo: string) => `${repo} 里的 GGUF 文件`,
|
|
164
|
+
noFiles: '这个仓库里没有 GGUF 文件。',
|
|
165
|
+
file: '文件',
|
|
166
|
+
size: '大小',
|
|
167
|
+
tag: '标签',
|
|
142
168
|
localDir: '本机 GGUF 目录',
|
|
143
169
|
cacheDir: '缓存目录',
|
|
144
170
|
noModels: '暂无可用的 GGUF 模型:可通过上方输入拉取,或将 .gguf 文件放入本机目录后点击重扫。',
|
|
@@ -213,6 +239,22 @@ const panelEn: typeof panelZh = {
|
|
|
213
239
|
reload: 'Rescan',
|
|
214
240
|
pull: 'Pull',
|
|
215
241
|
pullPlaceholder: 'HuggingFace repo, e.g. ggml-org/Qwen2.5-Coder-7B-Instruct-GGUF',
|
|
242
|
+
use: 'Use',
|
|
243
|
+
useTitle: 'Put this model into the model section',
|
|
244
|
+
search: 'Search',
|
|
245
|
+
searchPlaceholder: 'Search GGUF repositories on HuggingFace, e.g. qwen3',
|
|
246
|
+
searchFailed: (reason: string) => `Search failed: ${reason}`,
|
|
247
|
+
noRepos: 'No matching repositories.',
|
|
248
|
+
repo: 'Repository',
|
|
249
|
+
downloads: 'Downloads',
|
|
250
|
+
likes: 'Likes',
|
|
251
|
+
updated: 'Updated',
|
|
252
|
+
showFiles: 'Files',
|
|
253
|
+
filesOf: (repo: string) => `GGUF files in ${repo}`,
|
|
254
|
+
noFiles: 'This repository has no GGUF files.',
|
|
255
|
+
file: 'File',
|
|
256
|
+
size: 'Size',
|
|
257
|
+
tag: 'Tag',
|
|
216
258
|
localDir: 'Local GGUF directory',
|
|
217
259
|
cacheDir: 'Cache directory',
|
|
218
260
|
noModels: 'No models yet: pull one above, or drop GGUF files into the local directory and rescan.',
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** New connection names are portable directory names; unchanged historical keys are exempt. */
|
|
2
|
+
export function validateProviderName(name: string): string | null {
|
|
3
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(name))
|
|
4
|
+
return 'Use English letters, digits, - or _; start with a letter or digit. Spaces are not allowed.';
|
|
5
|
+
if (/^(con|prn|aux|nul|com[0-9]|lpt[0-9])$/i.test(name))
|
|
6
|
+
return 'This name is reserved by Windows.';
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** 密钥变量名缺省时的默认名:按端点名派生,`-` 折成 `_`,与 validateEntry 的环境变量名格式一致。 */
|
|
11
|
+
export function defaultSecretName(name: string): string {
|
|
12
|
+
return `CORTICO_KEY_${name.toUpperCase().replace(/-/g, '_')}`;
|
|
13
|
+
}
|