cortico 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -14
- package/package.json +1 -3
- package/src/boot.ts +18 -0
- package/src/bot.ts +41 -6
- package/src/core/README.md +4 -6
- 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 +15 -5
- package/src/core/loop.ts +55 -27
- package/src/core/session.ts +3 -2
- package/src/core/timers.ts +8 -6
- package/src/core/types.ts +19 -3
- package/src/core/util.ts +17 -1
- package/src/deploy.ts +4 -1
- package/src/extensions/README.md +6 -4
- package/src/extensions/dry-mount.ts +23 -2
- package/src/extensions/manifest.ts +21 -11
- package/src/extensions.ts +147 -23
- package/src/launcher.ts +23 -9
- package/src/protocol/open-responses/context-log.ts +37 -9
- package/src/providers/README.md +32 -8
- package/src/providers/base.ts +3 -1
- package/src/providers/configuration.ts +2 -1
- package/src/providers/console/hub.ts +270 -0
- package/src/providers/console/settings.ts +8 -18
- package/src/providers/console/types.ts +5 -0
- package/src/providers/hub-api.ts +3 -0
- package/src/providers/llamacpp/config.ts +8 -6
- package/src/providers/llamacpp/console/runtime-panel.ts +5 -1
- package/src/providers/llamacpp/console/server.ts +3 -1
- package/src/providers/llamacpp/index.ts +3 -1
- package/src/providers/llamacpp/native.ts +10 -5
- package/src/providers/llamacpp/strings.ts +6 -6
- package/src/providers/name.ts +8 -0
- package/src/providers/openai-responses-compat/config.ts +13 -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 +127 -0
- package/src/providers/openai-responses-compat/index.ts +38 -13
- package/src/providers/openai-responses-compat/native.ts +9 -4
- package/src/providers/openai-responses-compat/strings.ts +62 -1
- package/src/providers/registry.ts +5 -0
- package/src/providers/transport/responses-input.ts +59 -10
- package/src/web/README.md +11 -6
- package/src/web/auth.ts +80 -0
- package/src/web/client/console-pages/builtins/llm-settings/panel.ts +5 -4
- 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 +6 -8
- package/src/web/client/console-pages/host.ts +24 -7
- package/src/web/client/core/api.ts +5 -1
- package/src/web/client/core/router.ts +15 -0
- package/src/web/client/features/core/strings.ts +2 -2
- 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 +2 -2
- package/src/web/client/features/live/diagnostics.ts +32 -0
- package/src/web/client/features/live/index.ts +34 -11
- package/src/web/client/features/live/onboarding.ts +4 -1
- package/src/web/client/features/live/protocol.ts +1 -0
- package/src/web/client/features/live/strings.ts +20 -14
- package/src/web/client/features/live/timeline.ts +2 -1
- package/src/web/client/features/providers/detail.ts +262 -0
- package/src/web/client/features/providers/drafts.ts +23 -0
- package/src/web/client/features/providers/index.ts +173 -87
- package/src/web/client/features/providers/strings.ts +44 -17
- package/src/web/client/features/providers/types.ts +15 -0
- package/src/web/client/features/settings/general.ts +13 -0
- package/src/web/client/features/settings/index.ts +1 -0
- package/src/web/client/features/settings/strings.ts +6 -0
- package/src/web/client/features/worlds/index.ts +1 -0
- package/src/web/client/main.ts +4 -0
- package/src/web/client/shell/index.ts +28 -48
- package/src/web/client/shell/strings.ts +0 -22
- package/src/web/client/ui/icons.ts +14 -1
- package/src/web/client/ui/prompt-input.tsx +17 -5
- package/src/web/client/ui/strings.ts +0 -2
- package/src/web/console-pages.ts +1 -1
- package/src/web/diagnostics.ts +133 -0
- package/src/web/public/login.html +67 -0
- package/src/web/public/styles.css +143 -26
- package/src/web/server.ts +233 -19
- package/src/web/shared/client-panel.ts +4 -1
- package/src/web/shared/console-protocol.ts +4 -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 +69 -11
- package/src/worlds/minecraft/cell-facts.ts +226 -0
- package/src/worlds/minecraft/chests.ts +5 -0
- package/src/worlds/minecraft/containers.ts +325 -0
- package/src/worlds/minecraft/entity-facts.ts +31 -5
- package/src/worlds/minecraft/executor.ts +283 -10348
- package/src/worlds/minecraft/inventory.ts +268 -0
- package/src/worlds/minecraft/melee.ts +419 -0
- package/src/worlds/minecraft/mineflayer-fixes.ts +55 -1
- package/src/worlds/minecraft/placed-ledger.ts +152 -0
- package/src/worlds/minecraft/placement.ts +1038 -0
- package/src/worlds/minecraft/receipt.ts +340 -0
- package/src/worlds/minecraft/skill-context.ts +358 -0
- package/src/worlds/minecraft/skills-build.ts +1169 -0
- package/src/worlds/minecraft/skills-container.ts +1343 -0
- package/src/worlds/minecraft/skills-craft.ts +333 -0
- package/src/worlds/minecraft/skills-dig.ts +624 -0
- package/src/worlds/minecraft/skills-gather.ts +1230 -0
- package/src/worlds/minecraft/skills-interact.ts +1559 -0
- package/src/worlds/minecraft/tools.ts +331 -0
- package/src/worlds/minecraft/travel.ts +763 -0
- package/src/worlds/minecraft/until.ts +75 -0
- package/src/worlds/qq/normalize.ts +14 -0
- package/src/worlds/qq/world.ts +53 -7
- package/src/worlds/terminal/world.ts +5 -3
package/src/providers/base.ts
CHANGED
|
@@ -60,7 +60,7 @@ export interface ProviderInstance {
|
|
|
60
60
|
contextWindow?(model: string): number | undefined;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
/**
|
|
63
|
+
/** 一个端点此刻能不能发起生成。判断只看本地状态,不连上游。 */
|
|
64
64
|
export interface ProviderAvailability {
|
|
65
65
|
ready: boolean;
|
|
66
66
|
/** 不可用的原因,控制台语言;可用时不带。 */
|
|
@@ -70,6 +70,7 @@ export interface ProviderAvailability {
|
|
|
70
70
|
export interface ProviderModule {
|
|
71
71
|
id: string;
|
|
72
72
|
title: string;
|
|
73
|
+
description?: string;
|
|
73
74
|
defaultBaseUrl?: string;
|
|
74
75
|
/** Candidate endpoint URLs offered on the URL field. Candidates only: any URL is accepted. */
|
|
75
76
|
baseUrlSuggestions?: readonly string[];
|
|
@@ -83,6 +84,7 @@ export interface ProviderModule {
|
|
|
83
84
|
* Absent (or a field left out) = the tables above are shown as written.
|
|
84
85
|
*/
|
|
85
86
|
localize?(language: Language): {
|
|
87
|
+
description?: string;
|
|
86
88
|
reasoningTiers?: readonly ReasoningTier[];
|
|
87
89
|
serviceTiers?: readonly ServiceTier[];
|
|
88
90
|
temperatureNote?: string;
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { coerceGroupValues, getByPath, setByPath } from '../../core/config-schema.ts';
|
|
2
|
+
/** Connection configuration transactions serialize writers across deployments and roll back all touched files on failure. */
|
|
3
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { dirname, join, resolve } from 'node:path';
|
|
6
|
+
import type { CoreConfig, LLMProviderEntry } from '../../core/types.ts';
|
|
7
|
+
import type { Language } from '../../core/language.ts';
|
|
8
|
+
import { readJsonObject, updateJsonObject } from '../../config-file.ts';
|
|
9
|
+
import { readTextFile } from '../../core/util.ts';
|
|
10
|
+
import type { ProviderModule } from '../base.ts';
|
|
11
|
+
import { validateEntry } from '../configuration.ts';
|
|
12
|
+
import { validateProviderName } from '../name.ts';
|
|
13
|
+
import { providerModules, type ProviderRegistry } from '../registry.ts';
|
|
14
|
+
import type { ProviderSettings } from './settings.ts';
|
|
15
|
+
import { quotePrices } from '../pricebook.ts';
|
|
16
|
+
import { connectionGroup } from './config.ts';
|
|
17
|
+
|
|
18
|
+
export class ProviderHubError extends Error {
|
|
19
|
+
constructor(message: string, readonly status = 400) { super(message); }
|
|
20
|
+
}
|
|
21
|
+
export interface ConnectionSave {
|
|
22
|
+
name: string;
|
|
23
|
+
copyFrom?: { name: string; revision: string };
|
|
24
|
+
entry: LLMProviderEntry;
|
|
25
|
+
expectedRevision?: string;
|
|
26
|
+
secretValue?: string;
|
|
27
|
+
}
|
|
28
|
+
export class ProviderHub {
|
|
29
|
+
private readonly revisions = new Map<string, string>();
|
|
30
|
+
constructor(private readonly config: CoreConfig, private readonly registry: ProviderRegistry,
|
|
31
|
+
private readonly settings: ProviderSettings, private readonly file: string,
|
|
32
|
+
private readonly root: string, private readonly modules: readonly ProviderModule[] = providerModules) {}
|
|
33
|
+
|
|
34
|
+
private path(name: string): string {
|
|
35
|
+
const path = resolve(this.root, name);
|
|
36
|
+
if (!name || dirname(path) !== resolve(this.root)) throw new ProviderHubError('Invalid provider identifier.');
|
|
37
|
+
return path;
|
|
38
|
+
}
|
|
39
|
+
private revision(name: string): string {
|
|
40
|
+
const dir = this.path(name);
|
|
41
|
+
return createHash('sha256').update(['config.json', '.env'].map(file =>
|
|
42
|
+
existsSync(join(dir, file)) ? readFileSync(join(dir, file)).toString('base64') : '').join(':')).digest('hex');
|
|
43
|
+
}
|
|
44
|
+
private refresh(): void {
|
|
45
|
+
const entries: Record<string, LLMProviderEntry> = Object.create(null);
|
|
46
|
+
if (existsSync(this.root)) for (const dir of readdirSync(this.root, { withFileTypes: true })) {
|
|
47
|
+
if (!dir.isDirectory() || (dir.name === '.write-lock' || dir.name.startsWith('.transaction-') || dir.name.startsWith('.deleted-'))) continue;
|
|
48
|
+
const file = join(this.root, dir.name, 'config.json');
|
|
49
|
+
if (existsSync(file)) entries[dir.name] = JSON.parse(readTextFile(file));
|
|
50
|
+
}
|
|
51
|
+
for (const name of new Set([...Object.keys(entries), ...Object.keys(this.config.providers)])) {
|
|
52
|
+
const revision = entries[name] ? this.revision(name) : '';
|
|
53
|
+
if (JSON.stringify(entries[name]) !== JSON.stringify(this.config.providers[name]) || this.revisions.get(name) !== revision) this.registry.invalidate(name);
|
|
54
|
+
this.revisions.set(name, revision);
|
|
55
|
+
}
|
|
56
|
+
this.config.providers = entries;
|
|
57
|
+
const disk = readJsonObject(this.file);
|
|
58
|
+
if (typeof disk.activeProvider === 'string') this.config.activeProvider = disk.activeProvider;
|
|
59
|
+
}
|
|
60
|
+
moduleList(language: Language) {
|
|
61
|
+
return this.modules.map(module => ({ id: module.id, title: module.title,
|
|
62
|
+
description: module.localize?.(language)?.description ?? module.description ?? module.title, defaultBaseUrl: module.defaultBaseUrl ?? '',
|
|
63
|
+
reasoningTiers: module.localize?.(language)?.reasoningTiers ?? module.reasoningTiers,
|
|
64
|
+
effortSuggestions: module.effortSuggestions ?? [], serviceTiers: module.localize?.(language)?.serviceTiers ?? module.serviceTiers,
|
|
65
|
+
temperatureNote: module.localize?.(language)?.temperatureNote ?? module.temperatureNote,
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
|
+
private readiness(name: string, entry: LLMProviderEntry, language: Language) {
|
|
69
|
+
const module = this.modules.find(m => m.id === entry.kind);
|
|
70
|
+
if (!module) return { state: 'module-missing', reason: language === 'zh' ? '供应商模块不可用。' : 'Provider module is unavailable.' };
|
|
71
|
+
try { validateEntry(module, entry, language); }
|
|
72
|
+
catch (error) { return { state: 'invalid', reason: String(error) }; }
|
|
73
|
+
if (!entry.spec?.model || (entry.secret && this.settings.secretStatus(name, entry) === 'none'))
|
|
74
|
+
return { state: 'needs-setup', reason: !entry.spec?.model ? (language === 'zh' ? '请选择模型。' : 'Model is required.') : (language === 'zh' ? '请配置 API Key。' : 'API Key is required.') };
|
|
75
|
+
const available = module.availability?.(name, entry, language);
|
|
76
|
+
if (available && !available.ready) return { state: 'runtime-unavailable', reason: available.reason };
|
|
77
|
+
return { state: 'ready' };
|
|
78
|
+
}
|
|
79
|
+
current(language: Language) {
|
|
80
|
+
const name = this.config.activeProvider;
|
|
81
|
+
const entry = this.config.providers[name];
|
|
82
|
+
return entry ? { name, model: entry.spec?.model ?? null, module: entry.kind,
|
|
83
|
+
moduleTitle: this.modules.find(module => module.id === entry.kind)?.title ?? entry.kind,
|
|
84
|
+
baseUrl: entry.baseUrl, ready: this.readiness(name, entry, language).state === 'ready' } : null;
|
|
85
|
+
}
|
|
86
|
+
list(language: Language) {
|
|
87
|
+
this.refresh();
|
|
88
|
+
return { scope: createHash('sha256').update(resolve(this.file)).digest('hex'), active: this.config.activeProvider, providers: Object.entries(this.config.providers).map(([name, entry]) => ({
|
|
89
|
+
id: name, name, module: entry.kind, moduleTitle: this.modules.find(m => m.id === entry.kind)?.title ?? entry.kind,
|
|
90
|
+
model: entry.spec?.model ?? null, baseUrl: entry.baseUrl, active: name === this.config.activeProvider,
|
|
91
|
+
readiness: this.readiness(name, entry, language), revision: this.revision(name),
|
|
92
|
+
})) };
|
|
93
|
+
}
|
|
94
|
+
detail(name: string, language: Language) {
|
|
95
|
+
this.refresh();
|
|
96
|
+
const entry = this.config.providers[name];
|
|
97
|
+
if (!entry) throw new ProviderHubError('Provider does not exist.', 404);
|
|
98
|
+
return { name, entry: structuredClone(entry), revision: this.revision(name),
|
|
99
|
+
secretConfigured: this.settings.secretStatus(name, entry), readiness: this.readiness(name, entry, language),
|
|
100
|
+
references: this.references(name).map(file => dirname(file).split(/[\\/]/).at(-1)),
|
|
101
|
+
config: this.groups(name, entry, language),
|
|
102
|
+
quotes: entry.spec ? [{ model: entry.spec.model, quotes: this.quotes(entry) }] : [],
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
private quotes(entry: LLMProviderEntry) {
|
|
106
|
+
const module = this.modules.find(module => module.id === entry.kind);
|
|
107
|
+
const request = { model: entry.spec?.model };
|
|
108
|
+
const at = { startedAt: new Date().toISOString(), requestedServiceTier: entry.serviceTier ?? null };
|
|
109
|
+
return quotePrices(entry, request, at, module?.prices?.(entry, request, at) ?? []);
|
|
110
|
+
}
|
|
111
|
+
groups(name: string, entry: LLMProviderEntry, language: Language) {
|
|
112
|
+
const module = this.modules.find(m => m.id === entry.kind);
|
|
113
|
+
return [connectionGroup(name, entry, language), ...(module?.config?.(name, entry, language) ?? [])];
|
|
114
|
+
}
|
|
115
|
+
async preview(name: string, entry: LLMProviderEntry, panel: string, method: string, args: unknown[], language: Language) {
|
|
116
|
+
this.path(name);
|
|
117
|
+
const module = this.modules.find(module => module.id === entry.kind);
|
|
118
|
+
if (!module?.console) throw new ProviderHubError('Module panel unavailable.');
|
|
119
|
+
this.refresh();
|
|
120
|
+
const current = this.config.providers[name];
|
|
121
|
+
const persisted = !!current && JSON.stringify(current) === JSON.stringify(entry);
|
|
122
|
+
let draft = structuredClone(entry);
|
|
123
|
+
const contribution = module.console({ language, editing: !persisted,
|
|
124
|
+
entries: () => [{ name, entry: draft }],
|
|
125
|
+
instance: requested => {
|
|
126
|
+
if (requested !== name) throw new ProviderHubError('Foreign provider.');
|
|
127
|
+
return persisted ? this.registry.resolve(name) : this.registry.preview(name, draft);
|
|
128
|
+
},
|
|
129
|
+
save: (requested, next) => {
|
|
130
|
+
if (requested !== name || next.kind !== entry.kind) throw new ProviderHubError('Foreign provider.');
|
|
131
|
+
draft = structuredClone(next);
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
const result = await contribution.invoke?.(panel, method, args);
|
|
135
|
+
return { result, entry: draft };
|
|
136
|
+
}
|
|
137
|
+
private references(name: string): string[] {
|
|
138
|
+
const files = new Set([this.file]);
|
|
139
|
+
const parent = dirname(this.root);
|
|
140
|
+
for (const dir of readdirSync(parent, { withFileTypes: true })) {
|
|
141
|
+
if (!dir.isDirectory() || resolve(parent, dir.name) === resolve(this.root)) continue;
|
|
142
|
+
const file = join(parent, dir.name, 'config.json');
|
|
143
|
+
if (existsSync(file) && existsSync(join(parent, dir.name, 'deployment.json'))) files.add(file);
|
|
144
|
+
}
|
|
145
|
+
return [...files].filter(file => readJsonObject(file).activeProvider === name);
|
|
146
|
+
}
|
|
147
|
+
private locked<T>(work: () => T): T {
|
|
148
|
+
mkdirSync(this.root, { recursive: true });
|
|
149
|
+
const lock = join(this.root, '.write-lock');
|
|
150
|
+
try { mkdirSync(lock); } catch { throw new ProviderHubError('Another provider transaction is in progress. Retry after it completes.', 409); }
|
|
151
|
+
try { this.refresh(); return work(); } finally { rmSync(lock, { recursive: true, force: true }); }
|
|
152
|
+
}
|
|
153
|
+
private checkRevision(name: string, expected: unknown): void {
|
|
154
|
+
if (typeof expected !== 'string' || expected !== this.revision(name))
|
|
155
|
+
throw new ProviderHubError('Configuration changed in another page or bot. Reload before saving.', 409);
|
|
156
|
+
}
|
|
157
|
+
save(original: string | null, input: ConnectionSave, language: Language) {
|
|
158
|
+
return this.locked(() => {
|
|
159
|
+
const { name } = input;
|
|
160
|
+
if (typeof name !== 'string') throw new ProviderHubError('Provider name is required.');
|
|
161
|
+
const prior = original ? this.config.providers[original] : undefined;
|
|
162
|
+
if (original && !prior) throw new ProviderHubError('Provider does not exist.', 404);
|
|
163
|
+
if (original) this.checkRevision(original, input.expectedRevision);
|
|
164
|
+
if (name !== original) {
|
|
165
|
+
const problem = validateProviderName(name);
|
|
166
|
+
if (problem) throw new ProviderHubError(problem);
|
|
167
|
+
if (readdirSync(this.root).some(value => value.toLowerCase() === name.toLowerCase()))
|
|
168
|
+
throw new ProviderHubError('Provider name or directory already exists.', 409);
|
|
169
|
+
}
|
|
170
|
+
if (!input.entry || typeof input.entry !== 'object') throw new ProviderHubError('Provider configuration is required.');
|
|
171
|
+
if (prior && prior.kind !== input.entry.kind) throw new ProviderHubError('Provider module cannot be changed.');
|
|
172
|
+
const module = this.modules.find(m => m.id === input.entry.kind);
|
|
173
|
+
if (!module) throw new ProviderHubError('Provider module is unavailable.');
|
|
174
|
+
const requested = structuredClone(input.entry);
|
|
175
|
+
if (!requested.spec?.model) throw new ProviderHubError('Model is required.');
|
|
176
|
+
if (input.secretValue !== undefined && (typeof input.secretValue !== 'string' || !input.secretValue || /\s/.test(input.secretValue)))
|
|
177
|
+
throw new ProviderHubError('API Key must be nonempty and contain no whitespace.');
|
|
178
|
+
if (input.secretValue && !requested.secret) requested.secret = 'CORTICO_PROVIDER_API_KEY';
|
|
179
|
+
const entry = validateEntry(module, requested, language);
|
|
180
|
+
const prefix = `providers.${name}.`;
|
|
181
|
+
for (const group of this.groups(name, entry, language)) {
|
|
182
|
+
const values = Object.fromEntries(Object.keys(group.schema.properties).filter(path => path.startsWith(prefix))
|
|
183
|
+
.map(path => [path, getByPath(entry as unknown as Record<string, unknown>, path.slice(prefix.length))])
|
|
184
|
+
.filter(([, value]) => value !== undefined));
|
|
185
|
+
const result = coerceGroupValues(group, values, language);
|
|
186
|
+
if ('error' in result) throw new ProviderHubError(result.error);
|
|
187
|
+
for (const [path, value] of Object.entries(result.values)) setByPath(entry as unknown as Record<string, unknown>, path.slice(prefix.length), value);
|
|
188
|
+
}
|
|
189
|
+
validateEntry(module, entry, language);
|
|
190
|
+
let copiedSecret: Buffer | null = null;
|
|
191
|
+
let credentialSource = original;
|
|
192
|
+
if (!original && input.copyFrom) {
|
|
193
|
+
const source = this.config.providers[input.copyFrom.name];
|
|
194
|
+
if (!source) throw new ProviderHubError('Copy source no longer exists.', 409);
|
|
195
|
+
this.checkRevision(input.copyFrom.name, input.copyFrom.revision);
|
|
196
|
+
if (source.secret === entry.secret) {
|
|
197
|
+
credentialSource = input.copyFrom.name;
|
|
198
|
+
const file = join(this.path(input.copyFrom.name), '.env');
|
|
199
|
+
if (existsSync(file)) copiedSecret = readFileSync(file);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (entry.secret && !input.secretValue && (!credentialSource || this.settings.secretStatus(credentialSource, entry) === 'none'))
|
|
203
|
+
throw new ProviderHubError('API Key is required.');
|
|
204
|
+
const target = this.path(name);
|
|
205
|
+
const source = original ? this.path(original) : null;
|
|
206
|
+
const stage = join(this.root, `.transaction-${randomUUID()}`);
|
|
207
|
+
const refs = original && name !== original ? this.references(original) : [];
|
|
208
|
+
const backups = new Map<string, Buffer | null>();
|
|
209
|
+
for (const file of [...refs, ...(source ? [join(source, 'config.json'), join(source, '.env')] : [])])
|
|
210
|
+
backups.set(file, existsSync(file) ? readFileSync(file) : null);
|
|
211
|
+
let moved = false;
|
|
212
|
+
try {
|
|
213
|
+
if (!source) mkdirSync(stage);
|
|
214
|
+
const dir = source ?? stage;
|
|
215
|
+
updateJsonObject(join(dir, 'config.json'), raw => { for (const key of Object.keys(raw)) delete raw[key]; Object.assign(raw, entry); });
|
|
216
|
+
if (copiedSecret) writeFileSync(join(dir, '.env'), copiedSecret);
|
|
217
|
+
if (input.secretValue) {
|
|
218
|
+
const file = join(dir, '.env');
|
|
219
|
+
const lines = (existsSync(file) ? readTextFile(file) : '').split(/\r?\n/).filter(line => line.split('=')[0]?.trim() !== entry.secret);
|
|
220
|
+
lines.push(`${entry.secret}=${input.secretValue}`);
|
|
221
|
+
writeFileSync(file, lines.filter(Boolean).join('\n') + '\n');
|
|
222
|
+
}
|
|
223
|
+
if (source !== target) { renameSync(dir, target); moved = true; }
|
|
224
|
+
for (const file of refs) updateJsonObject(file, raw => { raw.activeProvider = name; });
|
|
225
|
+
} catch (error) {
|
|
226
|
+
if (moved) renameSync(target, source ?? stage);
|
|
227
|
+
for (const [file, contents] of backups) {
|
|
228
|
+
if (contents === null) rmSync(file, { force: true }); else writeFileSync(file, contents);
|
|
229
|
+
}
|
|
230
|
+
throw error;
|
|
231
|
+
} finally { if (existsSync(stage)) rmSync(stage, { recursive: true, force: true }); }
|
|
232
|
+
if (original) this.registry.invalidate(original);
|
|
233
|
+
this.registry.invalidate(name);
|
|
234
|
+
this.refresh();
|
|
235
|
+
return this.detail(name, language);
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
activate(name: string, language: Language): void {
|
|
239
|
+
this.locked(() => {
|
|
240
|
+
const entry = this.config.providers[name];
|
|
241
|
+
if (!entry) throw new ProviderHubError('Provider does not exist.', 404);
|
|
242
|
+
const readiness = this.readiness(name, entry, language);
|
|
243
|
+
if (readiness.state !== 'ready') throw new ProviderHubError(readiness.reason ?? readiness.state);
|
|
244
|
+
updateJsonObject(this.file, raw => { raw.activeProvider = name; });
|
|
245
|
+
this.config.activeProvider = name;
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
delete(name: string, revision: string): void {
|
|
249
|
+
this.locked(() => {
|
|
250
|
+
if (!this.config.providers[name]) throw new ProviderHubError('Provider does not exist.', 404);
|
|
251
|
+
this.checkRevision(name, revision);
|
|
252
|
+
const refs = this.references(name);
|
|
253
|
+
if (this.config.activeProvider === name || refs.length)
|
|
254
|
+
throw new ProviderHubError(`Provider is used by: ${refs.map(file => dirname(file).split(/[\\/]/).at(-1)).join(', ') || 'current bot'}`, 409);
|
|
255
|
+
const tomb = join(this.root, `.deleted-${randomUUID()}`);
|
|
256
|
+
renameSync(this.path(name), tomb);
|
|
257
|
+
this.registry.invalidate(name);
|
|
258
|
+
delete this.config.providers[name];
|
|
259
|
+
rmSync(tomb, { recursive: true, force: true });
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
async action(name: string, action: string, language: Language) {
|
|
263
|
+
this.refresh();
|
|
264
|
+
const entry = this.config.providers[name];
|
|
265
|
+
if (!entry) throw new ProviderHubError('Provider does not exist.', 404);
|
|
266
|
+
const source = this.settings.sources().find(source => source.id === `llm:${entry.kind}`);
|
|
267
|
+
if (!source) throw new ProviderHubError('Provider module is unavailable.');
|
|
268
|
+
return source.contribute(language).invoke!('settings', action === 'test' ? 'probe' : 'models', [{ name }]);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
@@ -26,15 +26,12 @@ import { readTextFile } from '../../core/util.ts';
|
|
|
26
26
|
import { responseRequest } from '../../protocol/open-responses/context-helpers.ts';
|
|
27
27
|
import { record } from '../../protocol/open-responses/context.ts';
|
|
28
28
|
import { text } from './strings.ts';
|
|
29
|
-
import type
|
|
29
|
+
import { PROBE_MAX_OUTPUT_TOKENS, type ProviderConsoleHost } from './types.ts';
|
|
30
30
|
import { connectionGroup } from './config.ts';
|
|
31
31
|
|
|
32
32
|
export type SecretStatus = 'env' | 'file' | 'none';
|
|
33
33
|
|
|
34
|
-
/**
|
|
35
|
-
const PROBE_MAX_OUTPUT_TOKENS = 256;
|
|
36
|
-
|
|
37
|
-
/** 从磁盘加载的密钥变量名按字面匹配。 */
|
|
34
|
+
/** 密钥变量名由操作员自由填写,也可能是直接写在磁盘上的任意名字,拼进正则前按字面转义。 */
|
|
38
35
|
function escapeRegExp(s: string): string {
|
|
39
36
|
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
40
37
|
}
|
|
@@ -250,6 +247,7 @@ export class ProviderSettings {
|
|
|
250
247
|
})) satisfies ConsolePageSource[];
|
|
251
248
|
}
|
|
252
249
|
|
|
250
|
+
/** 端点能不能用:通用条件由框架查,模块自己的条件由模块答。 */
|
|
253
251
|
availability(name: string, language: Language = 'zh'): ProviderAvailability {
|
|
254
252
|
const entry = this.config.providers[name];
|
|
255
253
|
if (!entry) return { ready: false, reason: text(language).unknownInstance };
|
|
@@ -263,7 +261,8 @@ export class ProviderSettings {
|
|
|
263
261
|
}
|
|
264
262
|
|
|
265
263
|
/**
|
|
266
|
-
*
|
|
264
|
+
* 这批端点里有没有一个能用。灯亮=有;悬停说明给出第一个可用的端点名,
|
|
265
|
+
* 或者最后一个端点的不可用原因。
|
|
267
266
|
*/
|
|
268
267
|
private availableLamp(
|
|
269
268
|
entries: ReadonlyArray<{ name: string }>,
|
|
@@ -281,7 +280,7 @@ export class ProviderSettings {
|
|
|
281
280
|
return { label: S.availableLamp, state: 'offline', hint: reason };
|
|
282
281
|
}
|
|
283
282
|
|
|
284
|
-
/**
|
|
283
|
+
/** 所有模块合起来有没有一个可用端点。控制台左栏「模型提供商」那一行点的就是它。 */
|
|
285
284
|
providersLamp(language: Language = 'zh'): ConsoleLamp {
|
|
286
285
|
return this.availableLamp(
|
|
287
286
|
this.modules.flatMap((module) => this.entries(module)),
|
|
@@ -312,16 +311,7 @@ export class ProviderSettings {
|
|
|
312
311
|
kind: 'llm',
|
|
313
312
|
label: module.title,
|
|
314
313
|
availability: 'active',
|
|
315
|
-
lamps: [
|
|
316
|
-
{
|
|
317
|
-
label: S.activeInstanceLamp,
|
|
318
|
-
state: entries.some((value) => value.name === this.config.activeProvider)
|
|
319
|
-
? 'online'
|
|
320
|
-
: 'offline',
|
|
321
|
-
},
|
|
322
|
-
this.availableLamp(this.entries(module), language),
|
|
323
|
-
...(extra.lamps ?? []),
|
|
324
|
-
],
|
|
314
|
+
lamps: [],
|
|
325
315
|
badges: [{ label: S.instancesBadge, value: String(entries.length) }, ...(extra.badges ?? [])],
|
|
326
316
|
panels: [
|
|
327
317
|
{
|
|
@@ -385,7 +375,7 @@ export class ProviderSettings {
|
|
|
385
375
|
const name = body.name;
|
|
386
376
|
if (method === 'create') {
|
|
387
377
|
this.assertNewName(name, language);
|
|
388
|
-
//
|
|
378
|
+
// 报价留空:不覆盖模块价目;模块也没给价目时,这条端点的调用只记 token,不记金额。
|
|
389
379
|
this.save(name, {
|
|
390
380
|
kind: module.id,
|
|
391
381
|
baseUrl: String(body.baseUrl || module.defaultBaseUrl || ''),
|
|
@@ -2,9 +2,14 @@ import type { LLMProviderEntry } from '../../core/types.ts';
|
|
|
2
2
|
import type { Language } from '../../core/language.ts';
|
|
3
3
|
import type { ProviderInstance } from '../base.ts';
|
|
4
4
|
|
|
5
|
+
/** Output token limit for the connectivity probe and module probes. */
|
|
6
|
+
export const PROBE_MAX_OUTPUT_TOKENS = 256;
|
|
7
|
+
|
|
5
8
|
export interface ProviderConsoleHost {
|
|
6
9
|
/** Console language for panel titles, receipts and error texts. */
|
|
7
10
|
readonly language: Language;
|
|
11
|
+
/** Configuration edits return to the browser draft; runtime mutations require a saved connection. */
|
|
12
|
+
readonly editing?: boolean;
|
|
8
13
|
entries(): Array<{ name: string; entry: LLMProviderEntry }>;
|
|
9
14
|
instance(name: string): ProviderInstance;
|
|
10
15
|
save(name: string, entry: LLMProviderEntry): void;
|
|
@@ -7,26 +7,28 @@ import { panel } from './strings.ts';
|
|
|
7
7
|
export function runtimeConfig(name: string, entry: LLMProviderEntry, language: Language): ConfigGroup[] {
|
|
8
8
|
if (!llamacppOptions(entry).runtime) return [];
|
|
9
9
|
const S = panel[language];
|
|
10
|
-
const group = (
|
|
10
|
+
const group = (
|
|
11
|
+
id: string, title: string, description: string, properties: Record<string, ConfigProperty>,
|
|
12
|
+
): ConfigGroup => ({
|
|
11
13
|
id: `llm.llamacpp.${name}.${id}`,
|
|
12
14
|
owner: 'provider:llamacpp',
|
|
13
15
|
schema: {
|
|
14
|
-
type: 'object', title,
|
|
16
|
+
type: 'object', title, description,
|
|
15
17
|
properties: Object.fromEntries(Object.entries(properties).map(([path, property]) => [
|
|
16
18
|
`providers.${name}.options.${path}`, { ...property, 'x-hot': true },
|
|
17
19
|
])),
|
|
18
20
|
},
|
|
19
21
|
});
|
|
20
22
|
return [
|
|
21
|
-
group('runtime', S.runtimeSection, {
|
|
23
|
+
group('runtime', S.runtimeSection, S.runtimeSectionDesc, {
|
|
22
24
|
'runtime.release': { type: 'string', title: S.release, description: S.releaseHint },
|
|
23
|
-
'runtime.backend': { type: 'string', title: S.backend, enum: backendChoices() },
|
|
25
|
+
'runtime.backend': { type: 'string', title: S.backend, enum: backendChoices(), description: S.backendHint },
|
|
24
26
|
'runtime.runtimeDir': {
|
|
25
27
|
type: 'string', title: S.runtimeDir, description: S.runtimeDirHint, 'x-path': { kind: 'directory' },
|
|
26
28
|
},
|
|
27
29
|
}),
|
|
28
|
-
group('launch', S.launchSection, {
|
|
29
|
-
'launch.contextSize': { type: 'integer', title: S.contextSize, minimum: 1
|
|
30
|
+
group('launch', S.launchSection, S.launchSectionDesc, {
|
|
31
|
+
'launch.contextSize': { type: 'integer', title: S.contextSize, minimum: 1 },
|
|
30
32
|
'launch.nGpuLayers': { type: 'integer', title: S.nGpuLayers, minimum: 0 },
|
|
31
33
|
'launch.parallel': { type: 'integer', title: S.parallel, minimum: 1 },
|
|
32
34
|
'launch.extraArgs': { type: 'string', title: S.extraArgs, description: S.extraArgsHint },
|
|
@@ -59,12 +59,16 @@ export const runtimePanel: ConsolePanel = {
|
|
|
59
59
|
|
|
60
60
|
function renderConfig(row: Row, index: number, body: HTMLElement): void {
|
|
61
61
|
const { group, values } = row.config[index];
|
|
62
|
-
body.append(ui.section(group.schema.title));
|
|
62
|
+
body.append(ui.section(group.schema.title, group.schema.description));
|
|
63
63
|
for (const [path, property] of Object.entries(group.schema.properties)) {
|
|
64
64
|
const field = configField(ui, property, values[path], () => {
|
|
65
65
|
if (field.read) void saveConfig(group.id, { [path]: field.read() });
|
|
66
66
|
}, ctx.signal);
|
|
67
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
|
+
}
|
|
68
72
|
body.append(ui.field(property.title, field.node));
|
|
69
73
|
if (property.description) body.append(ui.msgline(property.description));
|
|
70
74
|
}
|
|
@@ -85,7 +85,7 @@ export function llamacppConsole(host: ProviderConsoleHost): Partial<ConsolePageC
|
|
|
85
85
|
return { ok: true };
|
|
86
86
|
}
|
|
87
87
|
if (method === 'disable') {
|
|
88
|
-
await control(name).runtime.stop(host.language);
|
|
88
|
+
if (!host.editing) await control(name).runtime.stop(host.language);
|
|
89
89
|
const entry = entryOf(name);
|
|
90
90
|
const { runtime: _runtime, launch: _launch, ...rest } = entry.options ?? {};
|
|
91
91
|
host.save(name, { ...entry, options: rest });
|
|
@@ -115,6 +115,7 @@ export function llamacppConsole(host: ProviderConsoleHost): Partial<ConsolePageC
|
|
|
115
115
|
});
|
|
116
116
|
return { ok: true };
|
|
117
117
|
}
|
|
118
|
+
if (host.editing) throw new Error(host.language === 'zh' ? '请先保存配置,再执行运行时操作。' : 'Save configuration before runtime operations.');
|
|
118
119
|
if (method === 'install') {
|
|
119
120
|
await control(name).runtime.install(host.language);
|
|
120
121
|
return { ok: true };
|
|
@@ -139,6 +140,7 @@ export function llamacppConsole(host: ProviderConsoleHost): Partial<ConsolePageC
|
|
|
139
140
|
await catalog.list(true);
|
|
140
141
|
return { ok: true };
|
|
141
142
|
}
|
|
143
|
+
if (host.editing) throw new Error(host.language === 'zh' ? '请先保存配置,再操作模型。' : 'Save configuration before model operations.');
|
|
142
144
|
if (typeof value.model !== 'string' || !value.model.trim()) throw new Error(S.modelIdRequired);
|
|
143
145
|
const model = value.model.trim();
|
|
144
146
|
if (method === 'pull') await catalog.download(model);
|
|
@@ -24,13 +24,14 @@ const reasoningTiers = (language: Language): ReasoningTier[] => {
|
|
|
24
24
|
export default {
|
|
25
25
|
id: 'llamacpp',
|
|
26
26
|
title: 'llama.cpp',
|
|
27
|
+
description: 'Connect to or manage local llama.cpp models.',
|
|
27
28
|
defaultBaseUrl: 'http://127.0.0.1:8090/v1',
|
|
28
29
|
baseUrlSuggestions: ['http://127.0.0.1:8090/v1', 'http://127.0.0.1:8080/v1'],
|
|
29
30
|
normalize: normalizeLlamaCpp,
|
|
30
31
|
console: llamacppConsole,
|
|
31
32
|
config: runtimeConfig,
|
|
32
33
|
reasoningTiers: reasoningTiers('zh'),
|
|
33
|
-
localize: (language) => ({ reasoningTiers: reasoningTiers(language) }),
|
|
34
|
+
localize: (language) => ({ description: language === 'zh' ? '连接或托管本地 llama.cpp 模型。' : 'Connect to or manage local llama.cpp models.', reasoningTiers: reasoningTiers(language) }),
|
|
34
35
|
serviceTiers: [],
|
|
35
36
|
validateEntry: (entry, language) => {
|
|
36
37
|
const S = text(language);
|
|
@@ -89,6 +90,7 @@ export default {
|
|
|
89
90
|
apiKey,
|
|
90
91
|
log: host.log,
|
|
91
92
|
media: { enabled: () => entry.multimodal === true, read: host.readBlob },
|
|
93
|
+
keepThinking: host.keepThinking,
|
|
92
94
|
}),
|
|
93
95
|
};
|
|
94
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> {
|
|
@@ -101,9 +101,9 @@ const panelZh = {
|
|
|
101
101
|
'启动失败时可在 Windows 安全中心核对拦截记录;若确认被拦截,请使用符合信任要求的运行时。',
|
|
102
102
|
server: '服务进程',
|
|
103
103
|
serverSection: '服务进程与端点',
|
|
104
|
-
serverSectionDesc: '',
|
|
104
|
+
serverSectionDesc: 'llama-server 本地服务管理与监听状态。',
|
|
105
105
|
runtimeSection: '二进制运行环境',
|
|
106
|
-
runtimeSectionDesc: '',
|
|
106
|
+
runtimeSectionDesc: '官方 llama.cpp 二进制包与运行依赖管理。',
|
|
107
107
|
releaseHint: '上游 release tag,如 b10930;换了要重新下载。',
|
|
108
108
|
backendHint: '按本机平台给出的官方构建;CUDA 版自动搭配上游的 cudart 包。',
|
|
109
109
|
runtimeDirHint: '填了就不下载:目录里要有 llama-server 及其动态库(自编译、Linux CUDA、签过名的构建)。',
|
|
@@ -141,7 +141,7 @@ const panelZh = {
|
|
|
141
141
|
pullPlaceholder: 'HuggingFace 仓库,如 ggml-org/Qwen2.5-Coder-7B-Instruct-GGUF',
|
|
142
142
|
localDir: '本机 GGUF 目录',
|
|
143
143
|
cacheDir: '缓存目录',
|
|
144
|
-
noModels: '
|
|
144
|
+
noModels: '暂无可用的 GGUF 模型:可通过上方输入拉取,或将 .gguf 文件放入本机目录后点击重扫。',
|
|
145
145
|
modelStatusLabel: {
|
|
146
146
|
loaded: '已加载',
|
|
147
147
|
loading: '加载中',
|
|
@@ -175,9 +175,9 @@ const panelEn: typeof panelZh = {
|
|
|
175
175
|
'If startup fails, check Windows Security for a block record. If a block is confirmed, use a runtime that meets the trust requirements.',
|
|
176
176
|
server: 'Server',
|
|
177
177
|
serverSection: 'Server Process & Endpoint',
|
|
178
|
-
serverSectionDesc: '',
|
|
178
|
+
serverSectionDesc: 'llama-server local process and listening status.',
|
|
179
179
|
runtimeSection: 'Binary Runtime Environment',
|
|
180
|
-
runtimeSectionDesc: '',
|
|
180
|
+
runtimeSectionDesc: 'Official llama.cpp binary and dependencies.',
|
|
181
181
|
releaseHint: 'Upstream release tag, such as b10930; changing it downloads another build.',
|
|
182
182
|
backendHint: 'Official builds for this platform; CUDA builds are paired with the upstream cudart package.',
|
|
183
183
|
runtimeDirHint: 'When set nothing is downloaded: the directory holds llama-server and its shared libraries (self-built, Linux CUDA, signed builds).',
|
|
@@ -215,7 +215,7 @@ const panelEn: typeof panelZh = {
|
|
|
215
215
|
pullPlaceholder: 'HuggingFace repo, e.g. ggml-org/Qwen2.5-Coder-7B-Instruct-GGUF',
|
|
216
216
|
localDir: 'Local GGUF directory',
|
|
217
217
|
cacheDir: 'Cache directory',
|
|
218
|
-
noModels: 'No
|
|
218
|
+
noModels: 'No models yet: pull one above, or drop GGUF files into the local directory and rescan.',
|
|
219
219
|
modelStatusLabel: {
|
|
220
220
|
loaded: 'loaded',
|
|
221
221
|
loading: 'loading',
|
|
@@ -0,0 +1,8 @@
|
|
|
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
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ConfigGroup } from '../../core/config-schema.ts';
|
|
2
2
|
import type { LLMProviderEntry } from '../../core/types.ts';
|
|
3
3
|
import type { Language } from '../../core/language.ts';
|
|
4
|
+
import { REASONING_REPLAYS, SYNTHETIC_REASONING_TEXT } from '../transport/responses-input.ts';
|
|
4
5
|
import { text } from './strings.ts';
|
|
5
6
|
|
|
6
7
|
export function protocolConfig(name: string, entry: LLMProviderEntry, language: Language): ConfigGroup[] {
|
|
@@ -11,9 +12,21 @@ export function protocolConfig(name: string, entry: LLMProviderEntry, language:
|
|
|
11
12
|
schema: {
|
|
12
13
|
type: 'object', title: name,
|
|
13
14
|
properties: {
|
|
15
|
+
[`providers.${name}.options.extraHeaders`]: {
|
|
16
|
+
type: 'object', title: language === 'zh' ? '附加请求头(JSON object)' : 'Extra headers (JSON object)',
|
|
17
|
+
},
|
|
18
|
+
[`providers.${name}.options.extraBody`]: {
|
|
19
|
+
type: 'object', title: language === 'zh' ? '附加请求体(JSON object)' : 'Extra request body (JSON object)',
|
|
20
|
+
},
|
|
14
21
|
[`providers.${name}.options.endpointPath`]: {
|
|
15
22
|
type: 'string', title: S.endpointPath, description: S.endpointPathDescription, 'x-hot': true,
|
|
16
23
|
},
|
|
24
|
+
[`providers.${name}.options.reasoningReplay`]: {
|
|
25
|
+
type: 'string', enum: [...REASONING_REPLAYS], title: S.reasoningReplay, description: S.reasoningReplayDescription, 'x-hot': true,
|
|
26
|
+
},
|
|
27
|
+
[`providers.${name}.options.syntheticReasoningText`]: {
|
|
28
|
+
type: 'string', title: S.syntheticReasoningText, description: S.syntheticReasoningTextDescription(SYNTHETIC_REASONING_TEXT), 'x-hot': true,
|
|
29
|
+
},
|
|
17
30
|
},
|
|
18
31
|
},
|
|
19
32
|
}];
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ConsoleClientBundle } from '../../../web/shared/client-panel.ts';
|
|
2
|
+
import { reasoningPanel } from './reasoning-panel.ts';
|
|
3
|
+
|
|
4
|
+
// The endpoint table (`builtin: 'llm-settings'`) is the console's own; only the reasoning section ships here.
|
|
5
|
+
export default { panels: { reasoning: reasoningPanel } } satisfies ConsoleClientBundle;
|