cortico 0.1.1 → 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 +3 -3
- package/package.json +1 -1
- package/src/boot.ts +18 -0
- package/src/bot.ts +40 -5
- package/src/core/README.md +4 -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 +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 +18 -2
- 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 +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 -7
- package/src/providers/base.ts +2 -0
- package/src/providers/console/config.ts +21 -0
- package/src/providers/console/hub.ts +270 -0
- package/src/providers/console/settings.ts +11 -19
- 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/runtime-panel.ts +36 -73
- package/src/providers/llamacpp/console/server.ts +23 -2
- 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/name.ts +8 -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 +127 -0
- package/src/providers/openai-responses-compat/index.ts +40 -12
- 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 +12 -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 +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 +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/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 +122 -18
- package/src/web/server.ts +227 -21
- package/src/web/shared/client-panel.ts +3 -0
- package/src/web/shared/console-protocol.ts +3 -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/mineflayer-fixes.ts +55 -1
- package/src/worlds/qq/normalize.ts +14 -0
- package/src/worlds/qq/world.ts +53 -7
- package/src/worlds/terminal/world.ts +3 -4
package/src/deploy.ts
CHANGED
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
* 部署文件中的 providers 字段不参与合并。Core、Persona 与 World 的默认值由各自所有者提供。
|
|
4
4
|
* 运行时共享合并后的配置对象,控制台和调参工具原位更新。
|
|
5
5
|
*/
|
|
6
|
-
import { existsSync, mkdirSync, readdirSync,
|
|
6
|
+
import { existsSync, mkdirSync, readdirSync, statSync, writeFileSync } from 'node:fs';
|
|
7
7
|
import { isAbsolute, resolve } from 'node:path';
|
|
8
8
|
import type { CoreConfig, LLMProviderEntry } from './core/types.ts';
|
|
9
9
|
import { deepMerge, type LoadedConfig } from './core/config.ts';
|
|
10
10
|
import { secretReader } from './core/secrets.ts';
|
|
11
|
+
import { readTextFile } from './core/util.ts';
|
|
11
12
|
import { deploymentRoot, repoRoot as codeRepoRoot } from './paths.ts';
|
|
12
13
|
|
|
13
14
|
export type { LoadedConfig } from './core/config.ts';
|
|
@@ -90,7 +91,7 @@ function globalProviders(providersDir: string): Record<string, LLMProviderEntry>
|
|
|
90
91
|
const file = resolve(providersDir, name, 'config.json');
|
|
91
92
|
if (!existsSync(file)) continue;
|
|
92
93
|
try {
|
|
93
|
-
table[name] = JSON.parse(
|
|
94
|
+
table[name] = JSON.parse(readTextFile(file)) as LLMProviderEntry;
|
|
94
95
|
} catch (err) {
|
|
95
96
|
throw new Error(`${file} 解析失败:${err instanceof Error ? err.message : String(err)}`);
|
|
96
97
|
}
|
|
@@ -110,7 +111,7 @@ function packageWorldOverrides(pkgDir: string): Record<string, unknown> {
|
|
|
110
111
|
const file = resolve(ioDir, id, 'config.json');
|
|
111
112
|
if (!existsSync(file)) continue;
|
|
112
113
|
try {
|
|
113
|
-
worlds[id] = JSON.parse(
|
|
114
|
+
worlds[id] = JSON.parse(readTextFile(file));
|
|
114
115
|
} catch (err) {
|
|
115
116
|
throw new Error(`${file} 解析失败:${err instanceof Error ? err.message : String(err)}`);
|
|
116
117
|
}
|
|
@@ -137,7 +138,7 @@ export function loadDeployment<C extends CoreConfig>(
|
|
|
137
138
|
const providers = resolve(providersDir);
|
|
138
139
|
const cfgPath = resolve(dir, 'config.json');
|
|
139
140
|
const raw: Partial<C> & Record<string, unknown> = existsSync(cfgPath)
|
|
140
|
-
? (JSON.parse(
|
|
141
|
+
? (JSON.parse(readTextFile(cfgPath)) as Partial<C> & Record<string, unknown>)
|
|
141
142
|
: {};
|
|
142
143
|
// 部署文件不能覆盖共享端点表。
|
|
143
144
|
delete raw.providers;
|
package/src/extensions/README.md
CHANGED
|
@@ -19,7 +19,7 @@ World 调用 `create()` / `tools()` / `console()`,provider 调用 `create()`,bot
|
|
|
19
19
|
"keywords": ["cortico-world"], // npm 搜索按类关键字:cortico-world / cortico-provider / cortico-bot
|
|
20
20
|
"cortico": {
|
|
21
21
|
"kind": "world", // world | provider | bot
|
|
22
|
-
"api":
|
|
22
|
+
"api": 5, // 这一类的契约版本,与 EXTENSION_API_VERSIONS[kind] 相等才加载
|
|
23
23
|
"consoleClient": "dist/console.js", // 可选:预构建的面板 bundle,包内相对路径
|
|
24
24
|
"consoleStyle": "dist/console.css" // 可选:随 bundle 注入的样式
|
|
25
25
|
}
|
|
@@ -27,9 +27,11 @@ World 调用 `create()` / `tools()` / `console()`,provider 调用 `create()`,bot
|
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
`parseExtensionManifest(pkg)` 只做解析与校验,不碰文件系统;装载器与
|
|
30
|
-
`pnpm check:extension <dir>` 共用它。`api`
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
`pnpm check:extension <dir>` 共用它。`api` 与这一类的契约版本不等时不加载,扩展页说明哪一边旧。
|
|
31
|
+
|
|
32
|
+
`EXTENSION_API_VERSIONS` 按 kind 各记一个版本:`WorldDefinition` 不兼容变更只加 world,
|
|
33
|
+
`ProviderModule` 只加 provider,`BotDefinition`(连同 `BotParts`、`Persona`、`LoadedConfig`)只加 bot;
|
|
34
|
+
`ConsolePanelContext` 与其余共用接口变更时三类一起加一。同一次发布里的多处变更合计加一。
|
|
33
35
|
|
|
34
36
|
## 装载
|
|
35
37
|
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 在临时部署中检查扩展构造与声明接口,不调用 start()。
|
|
3
|
-
* World 使用默认配置、无密钥,检查 create、tools、envPromptVars、console
|
|
3
|
+
* World 使用默认配置、无密钥,检查 create、tools、envPromptVars、console、工具名冲突与配置路径;
|
|
4
4
|
* provider 使用测试端点 create,bot 使用测试部署 build。
|
|
5
5
|
* World 构造失败为错误;provider 仅在绑定端点时构造,测试条目不完整导致的失败记为警告。
|
|
6
6
|
* 临时文件位于 scratchDir,调用方负责创建与清理。
|
|
7
7
|
*/
|
|
8
8
|
import { existsSync, mkdirSync } from 'node:fs';
|
|
9
9
|
import { join } from 'node:path';
|
|
10
|
-
import type { CoreConfig, World } from '../core/types.ts';
|
|
10
|
+
import type { ConfigGroup, CoreConfig, World } from '../core/types.ts';
|
|
11
11
|
import { MODULE_LAMP_MAX } from '../core/types.ts';
|
|
12
12
|
import type { LoadedConfig } from '../core/config.ts';
|
|
13
13
|
import { CORE_DEFAULTS } from '../core/config.ts';
|
|
14
|
+
import { getByPath } from '../core/config-schema.ts';
|
|
14
15
|
import { RESERVED_FRAME_NAMES } from '../core/loop.ts';
|
|
15
16
|
import { nullLogger } from '../core/util.ts';
|
|
16
17
|
import type { Language } from '../core/language.ts';
|
|
@@ -38,6 +39,24 @@ const isPlainObject = (v: unknown): v is Record<string, unknown> =>
|
|
|
38
39
|
|
|
39
40
|
const message = (error: unknown): string => (error instanceof Error ? error.message : String(error));
|
|
40
41
|
|
|
42
|
+
/**
|
|
43
|
+
* 声明过的每个配置路径都要在 `defaults()` 里有对应项,且落在 prefix 指的段内。
|
|
44
|
+
* 缺默认值时控制台照样渲染旋钮、也照样写回 config.json,而读到的是代码里另一处的兜底值。
|
|
45
|
+
*/
|
|
46
|
+
function configPathProblems(group: ConfigGroup, defaults: Record<string, unknown>, prefix: string): string[] {
|
|
47
|
+
const problems: string[] = [];
|
|
48
|
+
for (const path of Object.keys(group.schema.properties ?? {})) {
|
|
49
|
+
if (!path.startsWith(prefix)) {
|
|
50
|
+
problems.push(`配置组「${group.id}」声明的「${path}」不在 ${prefix} 段里:写回按路径走,值会落到别人的段上。`);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (getByPath(defaults, path.slice(prefix.length)) === undefined) {
|
|
54
|
+
problems.push(`配置组「${group.id}」声明的「${path}」在 defaults() 里没有对应项:旋钮能改、能写回 config.json,读到的仍是代码里的兜底值。`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return problems;
|
|
58
|
+
}
|
|
59
|
+
|
|
41
60
|
/** 面板 id 的形状:控制台一页内的局部 id。 */
|
|
42
61
|
const PANEL_ID = /^[a-z0-9-]+$/;
|
|
43
62
|
/** 工具名带前缀:`<短名>_` 起头。 */
|
|
@@ -190,6 +209,7 @@ export async function dryMountWorld(def: WorldDefinition<WorldSection>, opts: Wo
|
|
|
190
209
|
}
|
|
191
210
|
for (const group of decl.config ?? []) {
|
|
192
211
|
if (group.owner !== `world:${def.id}`) warnings.push(`配置组「${group.id}」的 owner 是「${group.owner}」,World 的配置组 owner 应为 world:${def.id}。`);
|
|
212
|
+
failures.push(...configPathProblems(group, defaults, `worlds.${def.id}.`));
|
|
193
213
|
}
|
|
194
214
|
const keys = new Set<string>();
|
|
195
215
|
for (const doc of decl.promptDocs ?? []) {
|
|
@@ -385,6 +405,7 @@ export function dryMountBot(def: BotDefinition<CoreConfig>, opts: BotDryMountOpt
|
|
|
385
405
|
|
|
386
406
|
for (const group of parts.console?.configGroups ?? []) {
|
|
387
407
|
if (group.owner !== 'persona') warnings.push(`配置组「${group.id}」的 owner 是「${group.owner}」,Persona 的配置组 owner 应为 persona。`);
|
|
408
|
+
failures.push(...configPathProblems(group, config, ''));
|
|
388
409
|
}
|
|
389
410
|
return report;
|
|
390
411
|
}
|
|
@@ -1,18 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 解析 package.json 的 cortico 扩展声明,不访问文件系统;装载器与 check:extension 共用。
|
|
3
|
-
* kind 必须为 world、provider 或 bot;api
|
|
4
|
-
* WorldDefinition、ProviderModule、BotDefinition 及其可达接口或 ConsolePanelContext
|
|
5
|
-
* 发生不兼容变更时递增版本。
|
|
3
|
+
* kind 必须为 world、provider 或 bot;api 必须等于这一类的契约版本。
|
|
6
4
|
* consoleClient 和 consoleStyle 是预构建产物的包内相对路径;服务端分配 URL。
|
|
7
5
|
* 包必须使用 type=module,使扩展与框架通过同一 ESM 解析方式共享模块实例。
|
|
8
6
|
*/
|
|
9
7
|
|
|
10
|
-
/** 扩展接口发生不兼容变更时递增。 */
|
|
11
|
-
export const EXTENSION_API_VERSION = 4;
|
|
12
|
-
|
|
13
8
|
export type ExtensionKind = 'world' | 'provider' | 'bot';
|
|
14
9
|
export const EXTENSION_KINDS: readonly ExtensionKind[] = ['world', 'provider', 'bot'];
|
|
15
10
|
|
|
11
|
+
/**
|
|
12
|
+
* 三类扩展各自的契约版本。一类的接口不兼容变更只递增这一类:
|
|
13
|
+
* world 看 `WorldDefinition`,provider 看 `ProviderModule`,bot 看 `BotDefinition`
|
|
14
|
+
* (连同 `BotParts`、`Persona`、`LoadedConfig`);`ConsolePanelContext` 与其余共用接口
|
|
15
|
+
* 变更时三类一起加一。同一次发布里的多处变更合计加一。
|
|
16
|
+
*/
|
|
17
|
+
export const EXTENSION_API_VERSIONS: Readonly<Record<ExtensionKind, number>> = {
|
|
18
|
+
world: 5,
|
|
19
|
+
provider: 5,
|
|
20
|
+
bot: 5,
|
|
21
|
+
};
|
|
22
|
+
|
|
16
23
|
/** npm 上按类发现用的关键字。 */
|
|
17
24
|
export const EXTENSION_KEYWORDS: Readonly<Record<ExtensionKind, string>> = {
|
|
18
25
|
world: 'cortico-world',
|
|
@@ -79,12 +86,15 @@ export function parseExtensionManifest(pkg: ExtensionPackageJson): ExtensionMani
|
|
|
79
86
|
}
|
|
80
87
|
|
|
81
88
|
const api = m.api;
|
|
89
|
+
// kind 认不出时不比版本:每一类的契约版本各走各的,不知道是哪一类就没有可比的数。
|
|
90
|
+
const expected = kindOk ? EXTENSION_API_VERSIONS[kind as ExtensionKind] : undefined;
|
|
82
91
|
if (typeof api !== 'number' || !Number.isInteger(api) || api < 1) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
92
|
+
const note = expected === undefined ? '' : `(本框架的 ${kind} 契约是 v${expected})`;
|
|
93
|
+
reasons.push(`cortico.api 必须是正整数${note},现在是 ${JSON.stringify(api)}。`);
|
|
94
|
+
} else if (expected !== undefined && api < expected) {
|
|
95
|
+
reasons.push(`扩展按 ${kind} 契约 v${api} 编写,本框架的 ${kind} 契约是 v${expected}:扩展需要升级。`);
|
|
96
|
+
} else if (expected !== undefined && api > expected) {
|
|
97
|
+
reasons.push(`扩展要求 ${kind} 契约 v${api},本框架的 ${kind} 契约只到 v${expected}:框架需要升级。`);
|
|
88
98
|
}
|
|
89
99
|
|
|
90
100
|
const client = m.consoleClient;
|
package/src/extensions.ts
CHANGED
|
@@ -14,8 +14,14 @@ import type { CoreConfig, Logger } from './core/types.ts';
|
|
|
14
14
|
import type { BotDefinition } from './bot.ts';
|
|
15
15
|
import type { WorldDefinition, WorldSection } from './world.ts';
|
|
16
16
|
import type { ProviderModule } from './providers/base.ts';
|
|
17
|
-
import type {
|
|
17
|
+
import type {
|
|
18
|
+
ExtensionInfo,
|
|
19
|
+
ExtensionInstallTarget,
|
|
20
|
+
ExtensionPackageDetail,
|
|
21
|
+
ExtensionSearchHit,
|
|
22
|
+
} from './web/server.ts';
|
|
18
23
|
import {
|
|
24
|
+
EXTENSION_API_VERSIONS,
|
|
19
25
|
EXTENSION_KEYWORDS,
|
|
20
26
|
parseExtensionManifest,
|
|
21
27
|
type ExtensionConsoleAsset,
|
|
@@ -368,11 +374,59 @@ interface RegistrySearchResponse {
|
|
|
368
374
|
description?: string;
|
|
369
375
|
keywords?: string[];
|
|
370
376
|
date?: string;
|
|
377
|
+
license?: string;
|
|
371
378
|
links?: { npm?: string; homepage?: string; repository?: string };
|
|
372
379
|
publisher?: { username?: string };
|
|
373
380
|
};
|
|
381
|
+
/** 被依赖的包数。热门包回字符串,新包回数字。 */
|
|
382
|
+
dependents?: number | string;
|
|
374
383
|
downloads?: { monthly?: number };
|
|
375
384
|
}>;
|
|
385
|
+
total?: number;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** registry 的包文档(`GET /<name>`)。只列我们要读的字段。 */
|
|
389
|
+
interface RegistryPackument {
|
|
390
|
+
'dist-tags'?: Record<string, string>;
|
|
391
|
+
versions?: Record<string, ExtensionPackageJson & {
|
|
392
|
+
license?: string;
|
|
393
|
+
deprecated?: string;
|
|
394
|
+
engines?: Record<string, string>;
|
|
395
|
+
maintainers?: Array<{ username?: string; name?: string }>;
|
|
396
|
+
_npmUser?: { name?: string };
|
|
397
|
+
dist?: { unpackedSize?: number; fileCount?: number };
|
|
398
|
+
homepage?: string;
|
|
399
|
+
bugs?: { url?: string } | string;
|
|
400
|
+
repository?: { url?: string } | string;
|
|
401
|
+
}>;
|
|
402
|
+
/** `created`、`modified` 和每个版本号各一条发布时间 */
|
|
403
|
+
time?: Record<string, string>;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** 一页取满 250:registry 的上限(传更大也只回 250)。 */
|
|
407
|
+
const SEARCH_PAGE_SIZE = 250;
|
|
408
|
+
/**
|
|
409
|
+
* 最多翻四页。`keywords:` 过滤后的全集现在是个位数;真涨到一千条,说明关键字被滥用,
|
|
410
|
+
* 再往后翻的也不是操作员要找的包。
|
|
411
|
+
*/
|
|
412
|
+
const SEARCH_MAX_HITS = 1000;
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* registry 给的仓库地址是 npm 规范化过的 `git+https://….git`:浏览器不认这个 scheme。
|
|
416
|
+
* 收成可点的 https;认不出形状就原样返回,让操作员自己看。
|
|
417
|
+
*/
|
|
418
|
+
export function repositoryWebUrl(raw: string): string {
|
|
419
|
+
let url = raw.trim().replace(/^git\+/, '').replace(/^git:\/\//, 'https://');
|
|
420
|
+
const ssh = /^(?:ssh:\/\/)?git@([^:/]+)[:/](.+)$/.exec(url);
|
|
421
|
+
if (ssh) url = `https://${ssh[1]}/${ssh[2]}`;
|
|
422
|
+
url = url.replace(/\.git$/, '');
|
|
423
|
+
return /^https?:\/\//.test(url) ? url : raw;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** `{ url }` 或裸串两种写法都收。 */
|
|
427
|
+
function urlOf(v: { url?: string } | string | undefined): string | undefined {
|
|
428
|
+
const raw = typeof v === 'string' ? v : v?.url;
|
|
429
|
+
return raw && raw.trim() ? raw.trim() : undefined;
|
|
376
430
|
}
|
|
377
431
|
|
|
378
432
|
export interface ExtensionManagerOptions {
|
|
@@ -441,35 +495,105 @@ export class ExtensionManager {
|
|
|
441
495
|
return { dir: this.dir, extensions: out };
|
|
442
496
|
}
|
|
443
497
|
|
|
444
|
-
|
|
498
|
+
/**
|
|
499
|
+
* 列出 npm 上带这一类关键字的全部包。**不接文本查询**:registry 的 `text=` 在
|
|
500
|
+
* `keywords:` 过滤之下只影响排序不缩小结果(实测 `keywords:cortico-world 任意词`
|
|
501
|
+
* 仍返回同样 8 条),筛选交给控制台在整份结果上做。
|
|
502
|
+
*/
|
|
503
|
+
async search(kind: ExtensionKind = 'world'): Promise<ExtensionSearchHit[]> {
|
|
445
504
|
const keyword = EXTENSION_KEYWORDS[kind];
|
|
446
|
-
const text = `keywords:${keyword} ${query.trim()}`.trim();
|
|
447
|
-
const url = `${this.registry}/-/v1/search?text=${encodeURIComponent(text)}&size=50`;
|
|
448
|
-
const data = (await this.fetchJson(url)) as RegistrySearchResponse;
|
|
449
505
|
const installed = new Set(readInstalled(this.dir).map((p) => p.name));
|
|
450
506
|
const hits: ExtensionSearchHit[] = [];
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
links
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
507
|
+
const seen = new Set<string>();
|
|
508
|
+
for (let from = 0; from < SEARCH_MAX_HITS; from += SEARCH_PAGE_SIZE) {
|
|
509
|
+
const text = encodeURIComponent(`keywords:${keyword}`);
|
|
510
|
+
const url = `${this.registry}/-/v1/search?text=${text}&size=${SEARCH_PAGE_SIZE}&from=${from}`;
|
|
511
|
+
const data = (await this.fetchJson(url)) as RegistrySearchResponse;
|
|
512
|
+
const objects = data.objects ?? [];
|
|
513
|
+
for (const obj of objects) {
|
|
514
|
+
const p = obj.package;
|
|
515
|
+
if (!p?.name || !p.version || !(p.keywords ?? []).includes(keyword)) continue;
|
|
516
|
+
if (seen.has(p.name)) continue;
|
|
517
|
+
seen.add(p.name);
|
|
518
|
+
const repository = p.links?.repository;
|
|
519
|
+
hits.push({
|
|
520
|
+
name: p.name,
|
|
521
|
+
version: p.version,
|
|
522
|
+
description: p.description ?? '',
|
|
523
|
+
kind,
|
|
524
|
+
...(p.date ? { date: p.date } : {}),
|
|
525
|
+
...(p.publisher?.username ? { publisher: p.publisher.username } : {}),
|
|
526
|
+
...(p.license ? { license: p.license } : {}),
|
|
527
|
+
...(p.keywords?.length ? { keywords: p.keywords } : {}),
|
|
528
|
+
downloads: obj.downloads?.monthly ?? 0,
|
|
529
|
+
dependents: Number(obj.dependents) || 0,
|
|
530
|
+
links: {
|
|
531
|
+
...(p.links?.npm ? { npm: p.links.npm } : {}),
|
|
532
|
+
...(repository ? { repository: repositoryWebUrl(repository) } : {}),
|
|
533
|
+
...(p.links?.homepage ? { homepage: p.links.homepage } : {}),
|
|
534
|
+
},
|
|
535
|
+
installed: installed.has(p.name),
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
if (objects.length < SEARCH_PAGE_SIZE) break;
|
|
469
539
|
}
|
|
470
540
|
return hits;
|
|
471
541
|
}
|
|
472
542
|
|
|
543
|
+
/**
|
|
544
|
+
* 一个包的详情。控制台在操作员点开某张卡片时才调,因此这里取整份包文档
|
|
545
|
+
* (`GET /<name>`,比搜索结果多出 cortico 声明、许可证、体积与版本史)。
|
|
546
|
+
* readme 不回传:一份 30KB 以上的 markdown,控制台也不渲染它。
|
|
547
|
+
*/
|
|
548
|
+
async packageInfo(name: string): Promise<ExtensionPackageDetail> {
|
|
549
|
+
if (!PACKAGE_NAME.test(name)) throw new Error(`不是合法的 npm 包名: ${name}`);
|
|
550
|
+
const doc = (await this.fetchJson(`${this.registry}/${name.replace('/', '%2F')}`)) as RegistryPackument;
|
|
551
|
+
const latest = doc['dist-tags']?.latest;
|
|
552
|
+
const v = latest ? doc.versions?.[latest] : undefined;
|
|
553
|
+
if (!latest || !v) throw new Error(`registry 没有给出 ${name} 的 latest 版本`);
|
|
554
|
+
|
|
555
|
+
const parsed = parseExtensionManifest(v);
|
|
556
|
+
const times = Object.entries(doc.time ?? {}).filter(([k]) => k !== 'created' && k !== 'modified');
|
|
557
|
+
const history = times
|
|
558
|
+
.sort((a, b) => (a[1] < b[1] ? 1 : -1))
|
|
559
|
+
.slice(0, 6)
|
|
560
|
+
.map(([version, date]) => ({ version, date }));
|
|
561
|
+
const spec = readInstalled(this.dir).find((p) => p.name === name)?.spec;
|
|
562
|
+
const repository = urlOf(v.repository);
|
|
563
|
+
const bugs = urlOf(v.bugs);
|
|
564
|
+
|
|
565
|
+
return {
|
|
566
|
+
name,
|
|
567
|
+
version: latest,
|
|
568
|
+
...(v.description ? { description: v.description } : {}),
|
|
569
|
+
...(v.license ? { license: v.license } : {}),
|
|
570
|
+
...(v.keywords?.length ? { keywords: v.keywords } : {}),
|
|
571
|
+
...(doc.time?.[latest] ? { published: doc.time[latest] } : {}),
|
|
572
|
+
...(doc.time?.created ? { created: doc.time.created } : {}),
|
|
573
|
+
versionCount: times.length,
|
|
574
|
+
history,
|
|
575
|
+
...(v.deprecated ? { deprecated: v.deprecated } : {}),
|
|
576
|
+
...(parsed.ok
|
|
577
|
+
? { manifest: parsed.manifest, frameworkApi: EXTENSION_API_VERSIONS[parsed.manifest.kind] }
|
|
578
|
+
: { problems: parsed.reasons }),
|
|
579
|
+
warnings: parsed.warnings,
|
|
580
|
+
...(v.engines?.node ? { engines: v.engines.node } : {}),
|
|
581
|
+
...(v.dist?.unpackedSize ? { unpackedSize: v.dist.unpackedSize } : {}),
|
|
582
|
+
...(v.dist?.fileCount ? { fileCount: v.dist.fileCount } : {}),
|
|
583
|
+
dependencies: Object.keys(v.dependencies ?? {}),
|
|
584
|
+
maintainers: (v.maintainers ?? []).map((m) => m.username ?? m.name ?? '').filter(Boolean),
|
|
585
|
+
...(v._npmUser?.name ? { publisher: v._npmUser.name } : {}),
|
|
586
|
+
links: {
|
|
587
|
+
npm: `https://www.npmjs.com/package/${name}`,
|
|
588
|
+
...(repository ? { repository: repositoryWebUrl(repository) } : {}),
|
|
589
|
+
...(v.homepage ? { homepage: v.homepage } : {}),
|
|
590
|
+
...(bugs ? { bugs } : {}),
|
|
591
|
+
},
|
|
592
|
+
installed: spec !== undefined,
|
|
593
|
+
...(spec !== undefined ? { installedSpec: spec } : {}),
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
|
|
473
597
|
async install(target: ExtensionInstallTarget): Promise<string> {
|
|
474
598
|
const spec = this.installSpec(target);
|
|
475
599
|
if (!existsSync(this.dir)) mkdirSync(this.dir, { recursive: true });
|
package/src/launcher.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { createBot } from './bot.ts';
|
|
|
13
13
|
import { createDeployment, ensureDeployment, listBots, loadDeployment } from './deploy.ts';
|
|
14
14
|
import { buildListing, type BotDefaults } from './deploy-listing.ts';
|
|
15
15
|
import { secretReader } from './core/secrets.ts';
|
|
16
|
-
import { announceDataDir, consumeBootFlags } from './boot.ts';
|
|
16
|
+
import { announceDataDir, consoleUrlOf, consumeBootFlags, listensOnEveryInterface, startsPaused } from './boot.ts';
|
|
17
17
|
import { extensionsDir, importBotDefinition, loadExtensions, locateBotPackage, readInstalled, type ActiveBotPackage } from './extensions.ts';
|
|
18
18
|
import { withWorlds } from './world.ts';
|
|
19
19
|
import { BUILTIN_WORLDS } from './worlds/index.ts';
|
|
@@ -160,6 +160,22 @@ async function main(): Promise<void> {
|
|
|
160
160
|
cfg.logging.file = levelArg as LogLevel;
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
// 监听地址与端口:--host= / --port= 只覆盖本次运行,不写回 config.json。
|
|
164
|
+
const hostArg = flagValue('host');
|
|
165
|
+
if (hostArg !== null) {
|
|
166
|
+
if (!hostArg.trim()) { console.error('--host 需要一个地址,如 0.0.0.0'); process.exit(1); }
|
|
167
|
+
cfg.web.host = hostArg.trim();
|
|
168
|
+
}
|
|
169
|
+
const portArg = flagValue('port');
|
|
170
|
+
if (portArg !== null) {
|
|
171
|
+
const port = Number(portArg);
|
|
172
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) {
|
|
173
|
+
console.error(`无效端口: ${portArg}(0–65535 的整数,0 由系统分配)`);
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
cfg.web.port = port;
|
|
177
|
+
}
|
|
178
|
+
|
|
163
179
|
// 重启标志必须在装配与 session 加载之前处理
|
|
164
180
|
consumeBootFlags(loaded.dataDir);
|
|
165
181
|
|
|
@@ -179,17 +195,15 @@ async function main(): Promise<void> {
|
|
|
179
195
|
const bot = createBot(loaded, definition, { extensions });
|
|
180
196
|
|
|
181
197
|
// 必须在启动主循环前暂停,避免首批事件提前投递。
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
process.env.CORTICO_START_PAUSED === 'true' ||
|
|
185
|
-
process.argv.includes('--paused');
|
|
186
|
-
if (startPaused) bot.core.bus.setPaused(true);
|
|
198
|
+
const paused = startsPaused();
|
|
199
|
+
if (paused) bot.core.bus.setPaused(true);
|
|
187
200
|
|
|
188
201
|
const { port } = await bot.start();
|
|
189
202
|
|
|
190
203
|
console.log(`\n Bot: ${botName}${cfg.displayName && cfg.displayName !== botName ? ` (${cfg.displayName})` : ''}`);
|
|
204
|
+
const consoleUrl = port === null ? null : consoleUrlOf(bot.webApp?.boundAddress ?? null, port);
|
|
191
205
|
if (port !== null) {
|
|
192
|
-
console.log(` 控制台:
|
|
206
|
+
console.log(` 控制台: ${consoleUrl}${listensOnEveryInterface(bot.webApp?.boundAddress ?? null) ? `(监听 ${bot.webApp?.boundAddress},所有网卡)` : ''}`);
|
|
193
207
|
const assetsProblem = webAssetsProblem(fileURLToPath(new URL('../dist/web', import.meta.url)));
|
|
194
208
|
if (assetsProblem) {
|
|
195
209
|
console.log(` ⚠ 控制台产物不完整(${assetsProblem});停止 bot 后运行 pnpm build:web`);
|
|
@@ -214,7 +228,7 @@ async function main(): Promise<void> {
|
|
|
214
228
|
const where = existsSync(envFile) ? `${envFile} 里也没有` : `${envFile} 不存在`;
|
|
215
229
|
console.log(` ⚠ 缺少 ${missingSecret}:进程环境里没有,${where};可在控制台「模型提供商」页修改密钥变量名或补填密钥`);
|
|
216
230
|
}
|
|
217
|
-
if (
|
|
231
|
+
if (paused) {
|
|
218
232
|
console.log(' ⏸ 已暂停');
|
|
219
233
|
}
|
|
220
234
|
console.log('');
|
|
@@ -223,7 +237,7 @@ async function main(): Promise<void> {
|
|
|
223
237
|
process.env.CORTICO_OPEN_BROWSER === '1' ||
|
|
224
238
|
process.env.CORTICO_OPEN_BROWSER === 'true' ||
|
|
225
239
|
process.argv.includes('--open');
|
|
226
|
-
if (openBrowserFlag &&
|
|
240
|
+
if (openBrowserFlag && consoleUrl !== null) openBrowser(consoleUrl);
|
|
227
241
|
|
|
228
242
|
// 此时限覆盖整个关机流程;各步骤的时限由 bot.shutdown() 管理。
|
|
229
243
|
const SHUTDOWN_GRACE_MS = 35_000;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { appendFileSync, closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
1
|
+
import { appendFileSync, closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, truncateSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { dirname } from 'node:path';
|
|
3
3
|
import type { ContextRecord } from './context.ts';
|
|
4
4
|
|
|
@@ -10,6 +10,20 @@ function freeze<T>(value: T): T {
|
|
|
10
10
|
return value;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
function isRecord(row: unknown): row is ContextRecord {
|
|
14
|
+
const r = row as ContextRecord | null;
|
|
15
|
+
return !!r && r.version === 2 && !!r.item && typeof r.item === 'object' && !Array.isArray(r.item)
|
|
16
|
+
&& !!r.context && typeof r.context === 'object' && !Array.isArray(r.context);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The repair `load()` applied to a final line that has no trailing newline. A line that does not
|
|
21
|
+
* parse is cut off; a complete record only gets its newline.
|
|
22
|
+
*/
|
|
23
|
+
export type ContextLoadRepair =
|
|
24
|
+
| { kind: 'torn-tail'; bytes: number }
|
|
25
|
+
| { kind: 'unterminated-tail' };
|
|
26
|
+
|
|
13
27
|
/** Immutable standard Items with runtime metadata stored in a separate field. */
|
|
14
28
|
export class ContextLog {
|
|
15
29
|
private entries: readonly ContextRecord[] = Object.freeze([]);
|
|
@@ -19,18 +33,32 @@ export class ContextLog {
|
|
|
19
33
|
|
|
20
34
|
get records(): readonly ContextRecord[] { return this.entries; }
|
|
21
35
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
36
|
+
/** Any other damaged line throws and leaves the file untouched. */
|
|
37
|
+
load(): ContextLoadRepair | null {
|
|
38
|
+
if (!existsSync(this.file)) { this.entries = Object.freeze([]); return null; }
|
|
39
|
+
const raw = readFileSync(this.file, 'utf8');
|
|
40
|
+
let body = raw;
|
|
41
|
+
let repair: ContextLoadRepair | null = null;
|
|
42
|
+
if (raw.length > 0 && !raw.endsWith('\n')) {
|
|
43
|
+
const cut = raw.lastIndexOf('\n') + 1;
|
|
44
|
+
const tail = raw.slice(cut);
|
|
45
|
+
let parsed: unknown = null;
|
|
46
|
+
try { parsed = JSON.parse(tail); } catch { /* torn */ }
|
|
47
|
+
if (isRecord(parsed)) repair = { kind: 'unterminated-tail' };
|
|
48
|
+
else { body = raw.slice(0, cut); repair = { kind: 'torn-tail', bytes: Buffer.byteLength(tail, 'utf8') }; }
|
|
49
|
+
}
|
|
50
|
+
const rows = body.split(/\r?\n/).filter(line => line.trim()).map((line, index) => {
|
|
51
|
+
let row: unknown;
|
|
52
|
+
try { row = JSON.parse(line); }
|
|
27
53
|
catch { throw new Error(`Invalid session JSON at ${this.file}:${index + 1}`); }
|
|
28
|
-
if (!row
|
|
29
|
-
|| !row.context || typeof row.context !== 'object' || Array.isArray(row.context))
|
|
30
|
-
throw new Error(`Invalid context record at ${this.file}:${index + 1}`);
|
|
54
|
+
if (!isRecord(row)) throw new Error(`Invalid context record at ${this.file}:${index + 1}`);
|
|
31
55
|
return freeze(row);
|
|
32
56
|
});
|
|
57
|
+
// Written only after every kept line validated.
|
|
58
|
+
if (repair?.kind === 'torn-tail') truncateSync(this.file, Buffer.byteLength(body, 'utf8'));
|
|
59
|
+
else if (repair) appendFileSync(this.file, '\n');
|
|
33
60
|
this.entries = Object.freeze(rows);
|
|
61
|
+
return repair;
|
|
34
62
|
}
|
|
35
63
|
|
|
36
64
|
append(entry: ContextRecord): ContextRecord {
|
package/src/providers/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- Owner: src/providers/base.ts, src/providers/registry.ts -->
|
|
1
|
+
<!-- Owner: src/providers/base.ts, src/providers/console/hub.ts, src/providers/hub-api.ts, src/providers/name.ts, src/providers/registry.ts, src/providers/console/settings.ts, src/providers/console/config.ts, src/providers/openai-responses-compat/config.ts, src/providers/transport/responses-input.ts, src/providers/transport/history.ts, src/providers/llamacpp/native.ts -->
|
|
2
2
|
|
|
3
3
|
# src/providers
|
|
4
4
|
|
|
@@ -22,14 +22,16 @@
|
|
|
22
22
|
## ProviderModule
|
|
23
23
|
|
|
24
24
|
`id` 必须等于目录名(扩展包里则是包声明的 id)。必填:`title`、`reasoningTiers`(空表 = 开放,
|
|
25
|
-
effort 收任意非空串)、`serviceTiers`、`create(name, entry, host)`。可选:`defaultBaseUrl` 与
|
|
25
|
+
effort 收任意非空串)、`serviceTiers`、`create(name, entry, host)`。可选:`description`、`defaultBaseUrl` 与
|
|
26
26
|
`baseUrlSuggestions`、`effortSuggestions`、`temperatureNote`、`localize()`、`normalize()`、
|
|
27
27
|
`validateEntry()`、`validateModel()`、`accepts()`(多模态判定)、`config()` 与 `console()`
|
|
28
28
|
(附加配置组与面板)、`prices()`、`estimateTokens()`、`contextOverflow()`。
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
`options.*`
|
|
32
|
-
|
|
30
|
+
地址、密钥变量名与图像开关不归模块:框架在 `console/config.ts` 里为每个端点声明这一组,
|
|
31
|
+
扩展来的模块照样有。模块自己的 `options.*` 走 `config()` 的配置组,面板在 `instance` 插槽里
|
|
32
|
+
用控制台的 schema 渲染器画同一份声明、经 `ctx.setConfig` 暂存到连接草稿——内建 llamacpp 的运行时与启动
|
|
33
|
+
两段走的是这条。`console()` 显式给空 `config` 表示这一页不另开配置页签,声明仍参与服务端
|
|
34
|
+
校验。启停与模型操作这类动作走面板 invoke。
|
|
33
35
|
|
|
34
36
|
`create()` 返回 `ProviderInstance`:`client`(实现 `respond`)、`listModels?`、`control?`、
|
|
35
37
|
`compatibilityKey?`、`start?` / `stop?`、`contextWindow?(model)`。
|
|
@@ -68,8 +70,11 @@ Core 侧:`activeProviderEntry()` / `activeSpec()` 每次现读;`contextWindowOf(
|
|
|
68
70
|
`LLMError`;每次请求尝试记录 `meters` 与 `charges`。
|
|
69
71
|
|
|
70
72
|
`responses-input.ts` 是原生 Responses 的无状态重放(system / developer 上提为
|
|
71
|
-
`instructions
|
|
72
|
-
|
|
73
|
+
`instructions`;思维链按 `reasoningReplay` 以签名块或 `reasoning_text` 回传,明文形态下没有记录来源
|
|
74
|
+
的调用前补一项合成推理);`chat.ts` + `native-input.ts` / `history.ts` 转换 Chat Completions 请求(历史
|
|
75
|
+
思维链回不回传由模块定:`renderMessagesWithMedia` 的 `keepReasoning` 决定出不出线,`dropPastThinking`
|
|
76
|
+
把合成开头之外的每条抹空);
|
|
77
|
+
`response-assembly.ts` 把两种流归一成 Open Responses 的 Item 流,
|
|
73
78
|
`finish_reason` 的 `length` / `content_filter` 落成 `incomplete_details.reason`;
|
|
74
79
|
`response-meters.ts` 把两种 usage 归一成 `TokenMeters`,缺项保持 null。
|
|
75
80
|
|
|
@@ -83,3 +88,23 @@ Core 侧:`activeProviderEntry()` / `activeSpec()` 每次现读;`contextWindowOf(
|
|
|
83
88
|
模块默认,快照带 sha256 id 与 `capturedAt`。计量键:`input` / `output` / `total` /
|
|
84
89
|
`cachedInput` / `uncachedInput` / `reasoning` 与 `detail:*`,单价按每百万 token。实际扣费在
|
|
85
90
|
`src/core/generation.ts` 的 `priceUsage()`,缺计量记 `amount: null`。
|
|
91
|
+
|
|
92
|
+
## 端点配置接口
|
|
93
|
+
|
|
94
|
+
`console/hub.ts` 的 `ProviderHub` 是控制台改端点的唯一入口,`hub-api.ts` 把它接到
|
|
95
|
+
`/api/providers`(聚合、按名读取、保存、删除、设为当前、测试、模型列表)与
|
|
96
|
+
`/api/provider-modules`(已注册模块)。列表保留 `kind` 没有对应模块的端点,active 与可用性
|
|
97
|
+
分别报告;状态帧的 `modelConnection` 带当前端点的名称、模型、模块与地址。
|
|
98
|
+
|
|
99
|
+
新名称由 `name.ts` 校验:英文字母、数字、`-` 或 `_`,首字符是字母或数字,不收空格与 Windows
|
|
100
|
+
设备名——名字就是 `<部署根>/providers/` 下的目录名。已在磁盘上的名字不改就继续有效。保存带
|
|
101
|
+
读取时的 revision(端点 `config.json` 与 `.env` 的 sha256),对不上返回 409。共享目录上的写锁
|
|
102
|
+
`.write-lock` 串行化各进程的写入。改名同时改目录和部署根内各 `deployment.json` 的
|
|
103
|
+
`activeProvider`,中途失败回滚已动过的文件和目录;还被引用的端点不能删。
|
|
104
|
+
|
|
105
|
+
保存一次校验模型、连接与模块配置,再写 `config.json` 和可选密钥;密钥进该端点的 `.env`,读取
|
|
106
|
+
接口不返回密钥值。写完清掉本进程的实例缓存,已绑定的请求与 fork 保持原客户端;其他进程在下次
|
|
107
|
+
重读共享配置时跟上。
|
|
108
|
+
|
|
109
|
+
`ProviderConsoleHost.editing` 表示这个实例来自还没保存的表单,模块据此拒绝运行时副作用,
|
|
110
|
+
`host.save` 把配置交回浏览器暂存。`ProviderRegistry.preview` 拿到的实例不进运行实例缓存。
|
package/src/providers/base.ts
CHANGED
|
@@ -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,21 @@
|
|
|
1
|
+
import type { ConfigGroup } from '../../core/config-schema.ts';
|
|
2
|
+
import type { LLMProviderEntry } from '../../core/types.ts';
|
|
3
|
+
import type { Language } from '../../core/language.ts';
|
|
4
|
+
import { text } from './strings.ts';
|
|
5
|
+
|
|
6
|
+
export function connectionGroup(name: string, entry: LLMProviderEntry, language: Language): ConfigGroup {
|
|
7
|
+
const S = text(language);
|
|
8
|
+
const prefix = `providers.${name}.`;
|
|
9
|
+
return {
|
|
10
|
+
id: `llm.${entry.kind}.${name}.connection`,
|
|
11
|
+
owner: `provider:${entry.kind}`,
|
|
12
|
+
schema: {
|
|
13
|
+
type: 'object', title: name, description: S.connectionDescription,
|
|
14
|
+
properties: {
|
|
15
|
+
[`${prefix}baseUrl`]: { type: 'string', title: S.baseUrl, 'x-hot': true },
|
|
16
|
+
[`${prefix}secret`]: { type: 'string', title: S.secret, description: S.secretDescription, 'x-hot': true },
|
|
17
|
+
[`${prefix}multimodal`]: { type: 'boolean', title: S.multimodal, 'x-hot': true },
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
}
|