@celestea/studio 2.7.1
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/LICENSE +21 -0
- package/dist/app.d.ts +111 -0
- package/dist/app.js +219 -0
- package/dist/auth/api-token.d.ts +76 -0
- package/dist/auth/api-token.js +172 -0
- package/dist/auth/htpasswd.d.ts +22 -0
- package/dist/auth/htpasswd.js +61 -0
- package/dist/auth/index.d.ts +14 -0
- package/dist/auth/index.js +14 -0
- package/dist/auth/page.d.ts +19 -0
- package/dist/auth/page.js +73 -0
- package/dist/auth/rate-limit.d.ts +27 -0
- package/dist/auth/rate-limit.js +47 -0
- package/dist/auth/token.d.ts +49 -0
- package/dist/auth/token.js +112 -0
- package/dist/config.d.ts +78 -0
- package/dist/config.js +72 -0
- package/dist/deployment.d.ts +92 -0
- package/dist/deployment.js +154 -0
- package/dist/fake-runtime-adapter.d.ts +37 -0
- package/dist/fake-runtime-adapter.js +276 -0
- package/dist/handlers/auth.d.ts +24 -0
- package/dist/handlers/auth.js +155 -0
- package/dist/handlers/common.d.ts +76 -0
- package/dist/handlers/common.js +127 -0
- package/dist/handlers/config-shape.d.ts +76 -0
- package/dist/handlers/config-shape.js +227 -0
- package/dist/handlers/config.d.ts +26 -0
- package/dist/handlers/config.js +161 -0
- package/dist/handlers/context-shape.d.ts +33 -0
- package/dist/handlers/context-shape.js +59 -0
- package/dist/handlers/dialog.d.ts +22 -0
- package/dist/handlers/dialog.js +292 -0
- package/dist/handlers/exec.d.ts +22 -0
- package/dist/handlers/exec.js +131 -0
- package/dist/handlers/fs-read.d.ts +63 -0
- package/dist/handlers/fs-read.js +159 -0
- package/dist/handlers/fs.d.ts +57 -0
- package/dist/handlers/fs.js +153 -0
- package/dist/handlers/grants-shape.d.ts +45 -0
- package/dist/handlers/grants-shape.js +130 -0
- package/dist/handlers/grants.d.ts +21 -0
- package/dist/handlers/grants.js +267 -0
- package/dist/handlers/health.d.ts +37 -0
- package/dist/handlers/health.js +139 -0
- package/dist/handlers/index.d.ts +40 -0
- package/dist/handlers/index.js +85 -0
- package/dist/handlers/permissions.d.ts +9 -0
- package/dist/handlers/permissions.js +183 -0
- package/dist/handlers/plugins.d.ts +18 -0
- package/dist/handlers/plugins.js +23 -0
- package/dist/handlers/prompts.d.ts +12 -0
- package/dist/handlers/prompts.js +120 -0
- package/dist/handlers/providers.d.ts +12 -0
- package/dist/handlers/providers.js +178 -0
- package/dist/handlers/questions.d.ts +23 -0
- package/dist/handlers/questions.js +115 -0
- package/dist/handlers/session-model.d.ts +38 -0
- package/dist/handlers/session-model.js +105 -0
- package/dist/handlers/session-move.d.ts +22 -0
- package/dist/handlers/session-move.js +188 -0
- package/dist/handlers/session-tools.d.ts +19 -0
- package/dist/handlers/session-tools.js +75 -0
- package/dist/handlers/sessions.d.ts +32 -0
- package/dist/handlers/sessions.js +242 -0
- package/dist/handlers/usage.d.ts +26 -0
- package/dist/handlers/usage.js +77 -0
- package/dist/handlers/worker.d.ts +12 -0
- package/dist/handlers/worker.js +82 -0
- package/dist/handlers/workspaces.d.ts +12 -0
- package/dist/handlers/workspaces.js +88 -0
- package/dist/harness.test-util.d.ts +101 -0
- package/dist/harness.test-util.js +176 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +33 -0
- package/dist/main.d.ts +13 -0
- package/dist/main.js +35 -0
- package/dist/plugins.d.ts +105 -0
- package/dist/plugins.js +97 -0
- package/dist/question-registry.d.ts +124 -0
- package/dist/question-registry.js +212 -0
- package/dist/question-rows.d.ts +32 -0
- package/dist/question-rows.js +43 -0
- package/dist/replay/compare.d.ts +47 -0
- package/dist/replay/compare.js +89 -0
- package/dist/replay/e2e-replay.d.ts +46 -0
- package/dist/replay/e2e-replay.js +91 -0
- package/dist/replay/expect-compact.d.ts +31 -0
- package/dist/replay/expect-compact.js +92 -0
- package/dist/replay/fixtures.d.ts +52 -0
- package/dist/replay/fixtures.js +53 -0
- package/dist/replay/host.d.ts +32 -0
- package/dist/replay/host.js +62 -0
- package/dist/replay/index.d.ts +16 -0
- package/dist/replay/index.js +16 -0
- package/dist/replay/probes.d.ts +55 -0
- package/dist/replay/probes.js +225 -0
- package/dist/replay/report.d.ts +13 -0
- package/dist/replay/report.js +103 -0
- package/dist/replay/session-e2e.d.ts +44 -0
- package/dist/replay/session-e2e.js +91 -0
- package/dist/routes.d.ts +47 -0
- package/dist/routes.js +59 -0
- package/dist/runtime/attachments-llm.d.ts +13 -0
- package/dist/runtime/attachments-llm.js +37 -0
- package/dist/runtime/boot-recovery.d.ts +32 -0
- package/dist/runtime/boot-recovery.js +71 -0
- package/dist/runtime/context-snapshot.d.ts +59 -0
- package/dist/runtime/context-snapshot.js +113 -0
- package/dist/runtime/engine-grants.d.ts +121 -0
- package/dist/runtime/engine-grants.js +338 -0
- package/dist/runtime/engine-permissions.d.ts +29 -0
- package/dist/runtime/engine-permissions.js +92 -0
- package/dist/runtime/engine-plugins.d.ts +189 -0
- package/dist/runtime/engine-plugins.js +292 -0
- package/dist/runtime/engine-profile.d.ts +28 -0
- package/dist/runtime/engine-profile.js +88 -0
- package/dist/runtime/engine-session.d.ts +9 -0
- package/dist/runtime/engine-session.js +9 -0
- package/dist/runtime/fallback-contract.d.ts +32 -0
- package/dist/runtime/fallback-contract.js +10 -0
- package/dist/runtime/fallback-host.d.ts +134 -0
- package/dist/runtime/fallback-host.js +299 -0
- package/dist/runtime/host-autowake.d.ts +79 -0
- package/dist/runtime/host-autowake.js +86 -0
- package/dist/runtime/image-downgrade.d.ts +49 -0
- package/dist/runtime/image-downgrade.js +90 -0
- package/dist/runtime/inbox-message.d.ts +11 -0
- package/dist/runtime/inbox-message.js +20 -0
- package/dist/runtime/index.d.ts +17 -0
- package/dist/runtime/index.js +17 -0
- package/dist/runtime/ledger-view.d.ts +40 -0
- package/dist/runtime/ledger-view.js +56 -0
- package/dist/runtime/llm-assembly.d.ts +66 -0
- package/dist/runtime/llm-assembly.js +114 -0
- package/dist/runtime/offline-llm.d.ts +71 -0
- package/dist/runtime/offline-llm.js +132 -0
- package/dist/runtime/provider-target.d.ts +10 -0
- package/dist/runtime/provider-target.js +10 -0
- package/dist/runtime/question-host.d.ts +33 -0
- package/dist/runtime/question-host.js +41 -0
- package/dist/runtime/question-view.d.ts +48 -0
- package/dist/runtime/question-view.js +76 -0
- package/dist/runtime/real-runtime-adapter.d.ts +95 -0
- package/dist/runtime/real-runtime-adapter.js +553 -0
- package/dist/runtime/recovery-audit.d.ts +81 -0
- package/dist/runtime/recovery-audit.js +95 -0
- package/dist/runtime/recovery-view.d.ts +36 -0
- package/dist/runtime/recovery-view.js +63 -0
- package/dist/runtime/session-compose.d.ts +235 -0
- package/dist/runtime/session-compose.js +350 -0
- package/dist/runtime/session-grants.d.ts +36 -0
- package/dist/runtime/session-grants.js +69 -0
- package/dist/runtime/session-lifecycle.d.ts +40 -0
- package/dist/runtime/session-lifecycle.js +86 -0
- package/dist/runtime/session-publisher.d.ts +40 -0
- package/dist/runtime/session-publisher.js +48 -0
- package/dist/runtime/session-release.d.ts +53 -0
- package/dist/runtime/session-release.js +80 -0
- package/dist/runtime/test-util.d.ts +78 -0
- package/dist/runtime/test-util.js +162 -0
- package/dist/runtime/watchdog-view.d.ts +33 -0
- package/dist/runtime/watchdog-view.js +45 -0
- package/dist/runtime/worker-bridge.d.ts +76 -0
- package/dist/runtime/worker-bridge.js +156 -0
- package/dist/runtime/worker-live.d.ts +22 -0
- package/dist/runtime/worker-live.js +25 -0
- package/dist/runtime/worker-recovery.d.ts +35 -0
- package/dist/runtime/worker-recovery.js +64 -0
- package/dist/runtime/worker-table.d.ts +58 -0
- package/dist/runtime/worker-table.js +70 -0
- package/dist/runtime-adapter.d.ts +421 -0
- package/dist/runtime-adapter.js +76 -0
- package/dist/serial-queue.d.ts +16 -0
- package/dist/serial-queue.js +22 -0
- package/dist/server.d.ts +44 -0
- package/dist/server.js +119 -0
- package/dist/settings.d.ts +23 -0
- package/dist/settings.js +31 -0
- package/dist/sse.d.ts +67 -0
- package/dist/sse.js +186 -0
- package/dist/static.d.ts +20 -0
- package/dist/static.js +120 -0
- package/dist/store/builtin-sections.d.ts +52 -0
- package/dist/store/builtin-sections.js +118 -0
- package/dist/store/celestea-home.d.ts +12 -0
- package/dist/store/celestea-home.js +11 -0
- package/dist/store/fs-json.d.ts +51 -0
- package/dist/store/fs-json.js +119 -0
- package/dist/store/grants-audit.d.ts +84 -0
- package/dist/store/grants-audit.js +109 -0
- package/dist/store/grants-service.d.ts +44 -0
- package/dist/store/grants-service.js +41 -0
- package/dist/store/grants-tokens.d.ts +62 -0
- package/dist/store/grants-tokens.js +105 -0
- package/dist/store/grants.d.ts +113 -0
- package/dist/store/grants.js +238 -0
- package/dist/store/index.d.ts +36 -0
- package/dist/store/index.js +36 -0
- package/dist/store/mode.d.ts +35 -0
- package/dist/store/mode.js +41 -0
- package/dist/store/permissions.d.ts +61 -0
- package/dist/store/permissions.js +139 -0
- package/dist/store/prompts-compose.d.ts +56 -0
- package/dist/store/prompts-compose.js +62 -0
- package/dist/store/prompts-template.d.ts +26 -0
- package/dist/store/prompts-template.js +99 -0
- package/dist/store/prompts.d.ts +142 -0
- package/dist/store/prompts.js +273 -0
- package/dist/store/provider-probe.d.ts +81 -0
- package/dist/store/provider-probe.js +154 -0
- package/dist/store/providers.d.ts +97 -0
- package/dist/store/providers.js +219 -0
- package/dist/store/result.d.ts +29 -0
- package/dist/store/result.js +30 -0
- package/dist/store/session-id.d.ts +119 -0
- package/dist/store/session-id.js +220 -0
- package/dist/store/session-meta.d.ts +41 -0
- package/dist/store/session-meta.js +66 -0
- package/dist/store/session-ops.d.ts +109 -0
- package/dist/store/session-ops.js +264 -0
- package/dist/store/session-tools.d.ts +42 -0
- package/dist/store/session-tools.js +69 -0
- package/dist/store/sessions.d.ts +162 -0
- package/dist/store/sessions.js +307 -0
- package/dist/store/validate.d.ts +13 -0
- package/dist/store/validate.js +38 -0
- package/dist/store/workspaces.d.ts +67 -0
- package/dist/store/workspaces.js +228 -0
- package/dist/user-questions.d.ts +58 -0
- package/dist/user-questions.js +157 -0
- package/dist/version.d.ts +14 -0
- package/dist/version.js +52 -0
- package/package.json +38 -0
- package/webdist/assets/index-Bngs7gUm.js +98 -0
- package/webdist/assets/index-Bu6ci_rN.css +1 -0
- package/webdist/assets/katex-CCbS2qAY.js +257 -0
- package/webdist/assets/mhchem-CnUN8HwY.js +1 -0
- package/webdist/build-meta.json +7 -0
- package/webdist/index.html +261 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt registry store — global `prompts.json` + per-workspace
|
|
3
|
+
* `<CELESTEA_HOME>/workspaces/<ws>/prompts.json` (W880; legacy
|
|
4
|
+
* `<ws>/.celestea-prompts.json` stays readable) (`contracts/data-files/prompts.schema.json`,
|
|
5
|
+
* `src/prompts.rs:560-880`).
|
|
6
|
+
*
|
|
7
|
+
* Tolerated-on-read, strict-on-write: a missing OR malformed registry file
|
|
8
|
+
* degrades to an empty one (the registry must never break compose or the UI),
|
|
9
|
+
* while every write validates the id and every override template before the
|
|
10
|
+
* file is touched. There are four registry levels — builtin → global →
|
|
11
|
+
* workspace → the bound prompt's `section_overrides` — and writing is
|
|
12
|
+
* `persist -> hot apply -> roll the file back when the apply fails`.
|
|
13
|
+
*/
|
|
14
|
+
import { readFileSync, rmSync } from "node:fs";
|
|
15
|
+
import { existsSync } from "node:fs";
|
|
16
|
+
import { readJsonIfExists, writeJsonAtomic } from "./fs-json.js";
|
|
17
|
+
import { badRequest, errText, fail, notFound, ok } from "./result.js";
|
|
18
|
+
import { validatePromptId } from "./validate.js";
|
|
19
|
+
import { ORDER_FALLBACK, validateTemplate } from "./prompts-template.js";
|
|
20
|
+
import { builtinRowsFor } from "./builtin-sections.js";
|
|
21
|
+
import { DEFAULT_SESSION_MODE } from "./mode.js";
|
|
22
|
+
import { promptsFileCandidates } from "./session-id.js";
|
|
23
|
+
const EMPTY = { sections: [], prompts: [], default_prompt: null };
|
|
24
|
+
function parseSections(raw) {
|
|
25
|
+
if (!Array.isArray(raw))
|
|
26
|
+
return [];
|
|
27
|
+
const out = [];
|
|
28
|
+
for (const row of raw) {
|
|
29
|
+
if (typeof row !== "object" || row === null)
|
|
30
|
+
continue;
|
|
31
|
+
const rec = row;
|
|
32
|
+
const id = rec["id"];
|
|
33
|
+
const template = rec["template"];
|
|
34
|
+
if (typeof id !== "string" || id === "" || typeof template !== "string")
|
|
35
|
+
continue;
|
|
36
|
+
out.push({
|
|
37
|
+
id,
|
|
38
|
+
name: typeof rec["name"] === "string" ? rec["name"] : id,
|
|
39
|
+
template,
|
|
40
|
+
order: typeof rec["order"] === "number" ? Math.trunc(rec["order"]) : ORDER_FALLBACK,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
function parsePrompts(raw) {
|
|
46
|
+
if (!Array.isArray(raw))
|
|
47
|
+
return [];
|
|
48
|
+
const out = [];
|
|
49
|
+
for (const row of raw) {
|
|
50
|
+
if (typeof row !== "object" || row === null)
|
|
51
|
+
continue;
|
|
52
|
+
const rec = row;
|
|
53
|
+
const id = rec["id"];
|
|
54
|
+
if (typeof id !== "string" || validatePromptId(id) !== null)
|
|
55
|
+
continue;
|
|
56
|
+
const overrides = {};
|
|
57
|
+
const rawOverrides = rec["section_overrides"];
|
|
58
|
+
if (typeof rawOverrides === "object" && rawOverrides !== null) {
|
|
59
|
+
for (const [k, v] of Object.entries(rawOverrides)) {
|
|
60
|
+
if (typeof v === "string")
|
|
61
|
+
overrides[k] = v;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
out.push({ id, name: typeof rec["name"] === "string" ? rec["name"] : id, section_overrides: overrides, is_default: rec["is_default"] === true });
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
export class PromptsStore {
|
|
69
|
+
globalFile;
|
|
70
|
+
constructor(globalFile) {
|
|
71
|
+
this.globalFile = globalFile;
|
|
72
|
+
}
|
|
73
|
+
scopeGlobal() {
|
|
74
|
+
return { kind: "global", workspace: null, file: this.globalFile, files: [this.globalFile] };
|
|
75
|
+
}
|
|
76
|
+
scopeWorkspace(name, wsPath) {
|
|
77
|
+
const files = promptsFileCandidates(wsPath);
|
|
78
|
+
return { kind: "workspace", workspace: name, file: files[0], files };
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Tolerant read: missing OR malformed both degrade to an empty registry. The
|
|
82
|
+
* first EXISTING candidate wins (canonical, then the legacy locations).
|
|
83
|
+
*/
|
|
84
|
+
read(scope) {
|
|
85
|
+
const file = scope.files.find((candidate) => existsSync(candidate));
|
|
86
|
+
if (file === undefined)
|
|
87
|
+
return { ...EMPTY, sections: [], prompts: [] };
|
|
88
|
+
const out = readJsonIfExists(file);
|
|
89
|
+
if (!out.exists || out.error !== undefined)
|
|
90
|
+
return { ...EMPTY, sections: [], prompts: [] };
|
|
91
|
+
const rec = (typeof out.value === "object" && out.value !== null ? out.value : {});
|
|
92
|
+
const def = rec["default_prompt"];
|
|
93
|
+
return {
|
|
94
|
+
sections: parseSections(rec["sections"]),
|
|
95
|
+
prompts: parsePrompts(rec["prompts"]),
|
|
96
|
+
default_prompt: typeof def === "string" && def !== "" ? def : null,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/** Raw file text (the rollback snapshot for a failed hot apply). */
|
|
100
|
+
snapshot(scope) {
|
|
101
|
+
return existsSync(scope.file) ? readFileSync(scope.file, "utf8") : null;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Roll a failed hot apply back to its snapshot.
|
|
105
|
+
*
|
|
106
|
+
* W815-1: a null snapshot means the scope file did NOT exist before the
|
|
107
|
+
* mutate, so "back to the snapshot" is "remove the file" — returning early
|
|
108
|
+
* left the fresh content on disk while the engine had rejected it (registry
|
|
109
|
+
* ahead of the engine). A present snapshot is restored verbatim.
|
|
110
|
+
*/
|
|
111
|
+
restore(scope, text) {
|
|
112
|
+
if (text === null) {
|
|
113
|
+
rmSync(scope.file, { force: true });
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
writeJsonAtomic(scope.file, JSON.parse(text));
|
|
117
|
+
}
|
|
118
|
+
persist(scope, data) {
|
|
119
|
+
try {
|
|
120
|
+
writeJsonAtomic(scope.file, data);
|
|
121
|
+
return ok(undefined);
|
|
122
|
+
}
|
|
123
|
+
catch (e) {
|
|
124
|
+
return fail(500, errText(e));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
find(scope, id) {
|
|
128
|
+
return this.read(scope).prompts.find((p) => p.id === id);
|
|
129
|
+
}
|
|
130
|
+
/** `default_prompt` of this scope, falling back to the global one. */
|
|
131
|
+
defaultFor(scope) {
|
|
132
|
+
const own = this.read(scope);
|
|
133
|
+
if (scope.kind === "workspace" && own.default_prompt !== null)
|
|
134
|
+
return { id: own.default_prompt, scope: "workspace" };
|
|
135
|
+
if (scope.kind === "workspace") {
|
|
136
|
+
const global = this.read(this.scopeGlobal());
|
|
137
|
+
return global.default_prompt === null ? null : { id: global.default_prompt, scope: "global" };
|
|
138
|
+
}
|
|
139
|
+
return own.default_prompt === null ? null : { id: own.default_prompt, scope: "global" };
|
|
140
|
+
}
|
|
141
|
+
promptList(scope) {
|
|
142
|
+
const ws = scope.kind === "workspace" ? this.read(scope) : { ...EMPTY, prompts: [] };
|
|
143
|
+
const global = this.read(this.scopeGlobal());
|
|
144
|
+
const wsIds = new Set(ws.prompts.map((p) => p.id));
|
|
145
|
+
const rows = global.prompts.map((p) => ({ ...p, scope: "global", shadowed: scope.kind === "workspace" && wsIds.has(p.id) }));
|
|
146
|
+
if (scope.kind === "workspace")
|
|
147
|
+
rows.push(...ws.prompts.map((p) => ({ ...p, scope: "workspace", shadowed: false })));
|
|
148
|
+
return rows;
|
|
149
|
+
}
|
|
150
|
+
/** GET /api/prompts body (handler fills `active_prompt`). */
|
|
151
|
+
list(scope, activePrompt) {
|
|
152
|
+
return {
|
|
153
|
+
ok: true,
|
|
154
|
+
scope: scope.kind,
|
|
155
|
+
workspace: scope.workspace,
|
|
156
|
+
global_file: this.globalFile,
|
|
157
|
+
// The listing has no mode of its own: it shows the `standard` variant
|
|
158
|
+
// (the UI mode selector is P1, and every P0 session defaults to standard).
|
|
159
|
+
sections: this.effective(scope, null, DEFAULT_SESSION_MODE).rows,
|
|
160
|
+
prompts: this.promptList(scope),
|
|
161
|
+
default_prompt: this.defaultFor(scope),
|
|
162
|
+
active_prompt: activePrompt,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
effective(scope, boundId, mode) {
|
|
166
|
+
return composeSections({
|
|
167
|
+
global: scope.kind === "workspace" ? this.read(this.scopeGlobal()) : this.read(scope),
|
|
168
|
+
workspace: scope.kind === "workspace" ? this.read(scope) : null,
|
|
169
|
+
bound: boundId === null ? null : (this.find(scope, boundId) ?? null),
|
|
170
|
+
mode,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* The registry-resolved sections (used by the sections listing and by
|
|
175
|
+
* `build_gen`). `mode` swaps the `tool_access` builtin VARIANT (W729/K6); it
|
|
176
|
+
* never adds or removes a row.
|
|
177
|
+
*/
|
|
178
|
+
sections(scope, boundId = null, mode = DEFAULT_SESSION_MODE) {
|
|
179
|
+
return this.effective(scope, boundId, mode).rows;
|
|
180
|
+
}
|
|
181
|
+
upsert(scope, req) {
|
|
182
|
+
const idError = validatePromptId(req.id);
|
|
183
|
+
if (idError !== null)
|
|
184
|
+
return badRequest(idError);
|
|
185
|
+
const overrides = req.section_overrides ?? {};
|
|
186
|
+
for (const [sectionId, template] of Object.entries(overrides)) {
|
|
187
|
+
const bad = validateTemplate(template);
|
|
188
|
+
if (bad !== null)
|
|
189
|
+
return badRequest(`section '${sectionId}': ${bad.error}`);
|
|
190
|
+
}
|
|
191
|
+
const data = this.read(scope);
|
|
192
|
+
const idx = data.prompts.findIndex((p) => p.id === req.id);
|
|
193
|
+
const previous = idx >= 0 ? data.prompts[idx] : undefined;
|
|
194
|
+
const isDefault = req.is_default ?? previous?.is_default ?? false;
|
|
195
|
+
const entry = { id: req.id, name: req.name, section_overrides: overrides, is_default: isDefault };
|
|
196
|
+
if (idx >= 0)
|
|
197
|
+
data.prompts[idx] = entry;
|
|
198
|
+
else
|
|
199
|
+
data.prompts.push(entry);
|
|
200
|
+
if (isDefault) {
|
|
201
|
+
for (const p of data.prompts)
|
|
202
|
+
if (p.id !== req.id)
|
|
203
|
+
p.is_default = false;
|
|
204
|
+
}
|
|
205
|
+
const saved = this.persist(scope, data);
|
|
206
|
+
if (!saved.ok)
|
|
207
|
+
return saved;
|
|
208
|
+
return ok({ id: req.id, scope: scope.kind, hot_applied: true });
|
|
209
|
+
}
|
|
210
|
+
remove(scope, rawId) {
|
|
211
|
+
const data = this.read(scope);
|
|
212
|
+
const idx = data.prompts.findIndex((p) => p.id === rawId);
|
|
213
|
+
if (idx < 0)
|
|
214
|
+
return notFound(`unknown prompt '${rawId}'`);
|
|
215
|
+
data.prompts.splice(idx, 1);
|
|
216
|
+
if (data.default_prompt === rawId)
|
|
217
|
+
data.default_prompt = null;
|
|
218
|
+
const saved = this.persist(scope, data);
|
|
219
|
+
if (!saved.ok)
|
|
220
|
+
return saved;
|
|
221
|
+
return ok({ scope: scope.kind, hot_applied: true });
|
|
222
|
+
}
|
|
223
|
+
setDefault(scope, rawId) {
|
|
224
|
+
const data = this.read(scope);
|
|
225
|
+
const idx = data.prompts.findIndex((p) => p.id === rawId);
|
|
226
|
+
if (idx < 0)
|
|
227
|
+
return notFound(`unknown prompt '${rawId}'`);
|
|
228
|
+
data.default_prompt = rawId;
|
|
229
|
+
for (const p of data.prompts)
|
|
230
|
+
p.is_default = p.id === rawId;
|
|
231
|
+
const saved = this.persist(scope, data);
|
|
232
|
+
if (!saved.ok)
|
|
233
|
+
return saved;
|
|
234
|
+
return ok({ default_prompt: rawId, scope: scope.kind, hot_applied: true });
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Four-level overlay: builtin -> global -> workspace -> bound overrides.
|
|
239
|
+
* A known id keeps its ORIGINAL order and only swaps its template; an unknown
|
|
240
|
+
* id is appended with ORDER_FALLBACK and sorted by `(order, id)`.
|
|
241
|
+
*/
|
|
242
|
+
export function composeSections(input) {
|
|
243
|
+
const rows = [];
|
|
244
|
+
const seen = new Map();
|
|
245
|
+
const push = (id, name, template, order, source) => {
|
|
246
|
+
const at = seen.get(id);
|
|
247
|
+
if (at === undefined) {
|
|
248
|
+
seen.set(id, rows.length);
|
|
249
|
+
rows.push({ id, name, template, order, source });
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
const row = rows[at];
|
|
253
|
+
row.template = template;
|
|
254
|
+
if (source !== "builtin")
|
|
255
|
+
row.source = source;
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
for (const s of builtinRowsFor(input.mode ?? DEFAULT_SESSION_MODE))
|
|
259
|
+
push(s.id, s.name, s.template, s.order, "builtin");
|
|
260
|
+
for (const s of input.global.sections)
|
|
261
|
+
push(s.id, s.name, s.template, s.order, "global");
|
|
262
|
+
if (input.workspace !== null)
|
|
263
|
+
for (const s of input.workspace.sections)
|
|
264
|
+
push(s.id, s.name, s.template, s.order, "workspace");
|
|
265
|
+
if (input.bound !== null) {
|
|
266
|
+
for (const [id, template] of Object.entries(input.bound.section_overrides)) {
|
|
267
|
+
push(id, id, template, ORDER_FALLBACK, input.global.sections.some((s) => s.id === id) ? "global" : "builtin");
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
rows.sort((a, b) => (a.order === b.order ? (a.id < b.id ? -1 : 1) : a.order - b.order));
|
|
271
|
+
const templates = new Map(rows.map((r) => [r.id, r.template]));
|
|
272
|
+
return { rows, templates };
|
|
273
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider model probe — `/api/providers/test` and
|
|
3
|
+
* `/api/providers/{id}/models/fetch` (`src/providers.rs:523-553,725-776`).
|
|
4
|
+
*
|
|
5
|
+
* Two rules carry the security weight:
|
|
6
|
+
* 1. **keyless same-origin borrow** — a provider with NO key whose
|
|
7
|
+
* normalized `base_url` equals the CURRENT generation's `base_url` may
|
|
8
|
+
* borrow the engine's own key for that one request. The borrowed key is
|
|
9
|
+
* never persisted, never echoed and never logged;
|
|
10
|
+
* 2. a keyless NON-same-origin provider reports `该提供商未配置 api_key`
|
|
11
|
+
* WITHOUT issuing a request at all (no key ever leaves the process for a
|
|
12
|
+
* provider that never had one).
|
|
13
|
+
*
|
|
14
|
+
* The HTTP client is injected so the contract is testable without network.
|
|
15
|
+
*/
|
|
16
|
+
import { HttpTargetPolicy } from "@celestea/tools";
|
|
17
|
+
import type { RequestFormat } from "./providers.js";
|
|
18
|
+
export declare const UNSUPPORTED_FORMAT = "\u8BE5\u8BF7\u6C42\u683C\u5F0F\u6682\u4E0D\u652F\u6301\u81EA\u52A8\u6D4B\u8BD5";
|
|
19
|
+
export declare const NO_API_KEY = "\u8BE5\u63D0\u4F9B\u5546\u672A\u914D\u7F6E api_key";
|
|
20
|
+
/**
|
|
21
|
+
* W815-13 (= W819-6): the target was refused by the deployment SSRF policy.
|
|
22
|
+
* Deliberately generic - the resolver reason names the resolved IP, and that
|
|
23
|
+
* internal address must not travel back to the caller.
|
|
24
|
+
*/
|
|
25
|
+
export declare const TARGET_FORBIDDEN = "\u8BE5\u63D0\u4F9B\u5546\u5730\u5740\u88AB\u7AD9\u70B9\u7684 SSRF \u7B56\u7565\u62D2\u7EDD\uFF08CELESTEA_HTTP_ALLOW / CELESTEA_HTTP_DENY\uFF09";
|
|
26
|
+
export interface ProbeResponse {
|
|
27
|
+
status: number;
|
|
28
|
+
text(): Promise<string>;
|
|
29
|
+
}
|
|
30
|
+
export interface ProbeInit {
|
|
31
|
+
method: string;
|
|
32
|
+
headers: Record<string, string>;
|
|
33
|
+
signal?: AbortSignal;
|
|
34
|
+
}
|
|
35
|
+
export type ProbeFetch = (url: string, init: ProbeInit) => Promise<ProbeResponse>;
|
|
36
|
+
export interface ProbeCandidate {
|
|
37
|
+
id: string;
|
|
38
|
+
base_url: string;
|
|
39
|
+
request_format: RequestFormat;
|
|
40
|
+
api_key: string | null;
|
|
41
|
+
}
|
|
42
|
+
export interface ProbeOptions {
|
|
43
|
+
/** Defaults to the global fetch; tests inject a recorder. */
|
|
44
|
+
fetch?: ProbeFetch;
|
|
45
|
+
/** The CURRENT generation's base_url + key (borrow source). */
|
|
46
|
+
engineBaseUrl: string;
|
|
47
|
+
engineKey: string | null;
|
|
48
|
+
timeoutMs?: number;
|
|
49
|
+
/**
|
|
50
|
+
* W815-13: the deployment SSRF target policy. Default:
|
|
51
|
+
* HttpTargetPolicy.fromEnv(env ?? process.env) - exactly the policy
|
|
52
|
+
* http_request mounts, so a denied target is never dialed.
|
|
53
|
+
*/
|
|
54
|
+
policy?: HttpTargetPolicy;
|
|
55
|
+
/** Env for the default policy (tests / injected deployments). */
|
|
56
|
+
env?: NodeJS.ProcessEnv;
|
|
57
|
+
}
|
|
58
|
+
export interface ProbeOutcome {
|
|
59
|
+
ok: boolean;
|
|
60
|
+
models?: Array<{
|
|
61
|
+
id: string;
|
|
62
|
+
}>;
|
|
63
|
+
error?: string;
|
|
64
|
+
}
|
|
65
|
+
/** Probe bodies are model lists; 1 MiB is far beyond any real answer. */
|
|
66
|
+
export declare const PROBE_MAX_BODY_BYTES: number;
|
|
67
|
+
/** Which key a probe would use, and whether it was borrowed. NEVER logged. */
|
|
68
|
+
export declare function resolveProbeKey(candidate: ProbeCandidate, opts: ProbeOptions): {
|
|
69
|
+
key: string | null;
|
|
70
|
+
borrowed: boolean;
|
|
71
|
+
};
|
|
72
|
+
/** GET `<base_url>/models`; every failure is a 200 body with `ok:false`. */
|
|
73
|
+
export declare function probeModels(candidate: ProbeCandidate, opts: ProbeOptions): Promise<ProbeOutcome>;
|
|
74
|
+
export interface TestOutcome {
|
|
75
|
+
ok: boolean;
|
|
76
|
+
latency_ms?: number;
|
|
77
|
+
model_count?: number;
|
|
78
|
+
error?: string;
|
|
79
|
+
}
|
|
80
|
+
/** POST /api/providers/test — same probe, plus latency and model count. */
|
|
81
|
+
export declare function testProvider(candidate: ProbeCandidate, opts: ProbeOptions, now?: () => number): Promise<TestOutcome>;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider model probe — `/api/providers/test` and
|
|
3
|
+
* `/api/providers/{id}/models/fetch` (`src/providers.rs:523-553,725-776`).
|
|
4
|
+
*
|
|
5
|
+
* Two rules carry the security weight:
|
|
6
|
+
* 1. **keyless same-origin borrow** — a provider with NO key whose
|
|
7
|
+
* normalized `base_url` equals the CURRENT generation's `base_url` may
|
|
8
|
+
* borrow the engine's own key for that one request. The borrowed key is
|
|
9
|
+
* never persisted, never echoed and never logged;
|
|
10
|
+
* 2. a keyless NON-same-origin provider reports `该提供商未配置 api_key`
|
|
11
|
+
* WITHOUT issuing a request at all (no key ever leaves the process for a
|
|
12
|
+
* provider that never had one).
|
|
13
|
+
*
|
|
14
|
+
* The HTTP client is injected so the contract is testable without network.
|
|
15
|
+
*/
|
|
16
|
+
import { HttpTargetPolicy, requestOnce } from "@celestea/tools";
|
|
17
|
+
import { normalizeBaseUrl } from "./providers.js";
|
|
18
|
+
import { errText } from "./result.js";
|
|
19
|
+
export const UNSUPPORTED_FORMAT = "该请求格式暂不支持自动测试";
|
|
20
|
+
export const NO_API_KEY = "该提供商未配置 api_key";
|
|
21
|
+
/**
|
|
22
|
+
* W815-13 (= W819-6): the target was refused by the deployment SSRF policy.
|
|
23
|
+
* Deliberately generic - the resolver reason names the resolved IP, and that
|
|
24
|
+
* internal address must not travel back to the caller.
|
|
25
|
+
*/
|
|
26
|
+
export const TARGET_FORBIDDEN = "该提供商地址被站点的 SSRF 策略拒绝(CELESTEA_HTTP_ALLOW / CELESTEA_HTTP_DENY)";
|
|
27
|
+
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
28
|
+
/** Probe bodies are model lists; 1 MiB is far beyond any real answer. */
|
|
29
|
+
export const PROBE_MAX_BODY_BYTES = 1024 * 1024;
|
|
30
|
+
/**
|
|
31
|
+
* The production transport of a probe: one policy-approved, PINNED hop over
|
|
32
|
+
* the same requestOnce http_request uses. pinnedIps makes the socket connect
|
|
33
|
+
* to the exact address resolveChecked authorized, so a DNS rebind between
|
|
34
|
+
* check and connect cannot carry the borrowed key to an internal address
|
|
35
|
+
* (W738 check-then-use).
|
|
36
|
+
*/
|
|
37
|
+
function pinnedProbeFetch(pinnedIps, timeoutMs) {
|
|
38
|
+
return async (url, init) => {
|
|
39
|
+
const result = await requestOnce({
|
|
40
|
+
url: new URL(url),
|
|
41
|
+
method: init.method,
|
|
42
|
+
headers: Object.entries(init.headers),
|
|
43
|
+
body: null,
|
|
44
|
+
timeoutMs,
|
|
45
|
+
maxBodyBytes: PROBE_MAX_BODY_BYTES,
|
|
46
|
+
pinnedIps,
|
|
47
|
+
});
|
|
48
|
+
return { status: result.status, text: () => Promise.resolve(result.body) };
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function head(text, n) {
|
|
52
|
+
return text.length <= n ? text : text.slice(0, n);
|
|
53
|
+
}
|
|
54
|
+
/** Which key a probe would use, and whether it was borrowed. NEVER logged. */
|
|
55
|
+
export function resolveProbeKey(candidate, opts) {
|
|
56
|
+
const own = candidate.api_key;
|
|
57
|
+
if (typeof own === "string" && own !== "")
|
|
58
|
+
return { key: own, borrowed: false };
|
|
59
|
+
const sameOrigin = normalizeBaseUrl(candidate.base_url) === normalizeBaseUrl(opts.engineBaseUrl);
|
|
60
|
+
if (sameOrigin && opts.engineKey !== null && opts.engineKey !== "")
|
|
61
|
+
return { key: opts.engineKey, borrowed: true };
|
|
62
|
+
return { key: null, borrowed: false };
|
|
63
|
+
}
|
|
64
|
+
function parseModels(text) {
|
|
65
|
+
let body;
|
|
66
|
+
try {
|
|
67
|
+
body = JSON.parse(text);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
const rows = Array.isArray(body) ? body : body.data;
|
|
73
|
+
if (!Array.isArray(rows))
|
|
74
|
+
return null;
|
|
75
|
+
const out = [];
|
|
76
|
+
for (const row of rows) {
|
|
77
|
+
const id = typeof row === "object" && row !== null ? row["id"] : undefined;
|
|
78
|
+
if (typeof id === "string" && id !== "")
|
|
79
|
+
out.push({ id });
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
/** GET `<base_url>/models`; every failure is a 200 body with `ok:false`. */
|
|
84
|
+
export async function probeModels(candidate, opts) {
|
|
85
|
+
if (candidate.request_format !== "chat_completions")
|
|
86
|
+
return { ok: false, error: UNSUPPORTED_FORMAT };
|
|
87
|
+
if ((candidate.base_url ?? "").trim() === "")
|
|
88
|
+
return { ok: false, error: "base_url is required" };
|
|
89
|
+
const { key } = resolveProbeKey(candidate, opts);
|
|
90
|
+
if (key === null)
|
|
91
|
+
return { ok: false, error: NO_API_KEY };
|
|
92
|
+
const url = `${candidate.base_url.replace(/\/+$/, "")}/models`;
|
|
93
|
+
// W815-13: authorize the target with the SAME policy http_request uses
|
|
94
|
+
// before a single byte leaves the process. An inactive policy (both env vars
|
|
95
|
+
// unset) allows everything, so it neither resolves nor pins and the historical
|
|
96
|
+
// host-fetch path is unchanged.
|
|
97
|
+
const policy = opts.policy ?? HttpTargetPolicy.fromEnv(opts.env ?? process.env);
|
|
98
|
+
let pinned = null;
|
|
99
|
+
try {
|
|
100
|
+
if (policy.active) {
|
|
101
|
+
const checked = await policy.resolveChecked(url);
|
|
102
|
+
if (checked.reason !== null)
|
|
103
|
+
return { ok: false, error: TARGET_FORBIDDEN };
|
|
104
|
+
pinned = checked.ips;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
catch (e) {
|
|
108
|
+
return { ok: false, error: errText(e) };
|
|
109
|
+
}
|
|
110
|
+
const doFetch = opts.fetch ??
|
|
111
|
+
(pinned === null
|
|
112
|
+
? globalThis.fetch
|
|
113
|
+
: pinnedProbeFetch(pinned, opts.timeoutMs ?? DEFAULT_TIMEOUT_MS));
|
|
114
|
+
// The pinned transport owns its own deadline; only the injected / host-fetch
|
|
115
|
+
// paths keep the AbortSignal timeout (no dangling timer on the pinned path).
|
|
116
|
+
const signal = opts.fetch === undefined && pinned !== null
|
|
117
|
+
? undefined
|
|
118
|
+
: AbortSignal.timeout(opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
119
|
+
let res;
|
|
120
|
+
try {
|
|
121
|
+
res = await doFetch(url, {
|
|
122
|
+
method: "GET",
|
|
123
|
+
headers: { authorization: `Bearer ${key}`, accept: "application/json" },
|
|
124
|
+
...(signal === undefined ? {} : { signal }),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
catch (e) {
|
|
128
|
+
return { ok: false, error: errText(e) };
|
|
129
|
+
}
|
|
130
|
+
let text;
|
|
131
|
+
try {
|
|
132
|
+
text = await res.text();
|
|
133
|
+
}
|
|
134
|
+
catch (e) {
|
|
135
|
+
return { ok: false, error: `summary response read failed: ${errText(e)}` };
|
|
136
|
+
}
|
|
137
|
+
if (res.status < 200 || res.status >= 300) {
|
|
138
|
+
return { ok: false, error: `HTTP ${res.status}: ${head(text, 300)}` };
|
|
139
|
+
}
|
|
140
|
+
const models = parseModels(text);
|
|
141
|
+
if (models === null) {
|
|
142
|
+
return { ok: false, error: `response is not JSON (invalid body); body head: ${head(text, 200)}` };
|
|
143
|
+
}
|
|
144
|
+
return { ok: true, models };
|
|
145
|
+
}
|
|
146
|
+
/** POST /api/providers/test — same probe, plus latency and model count. */
|
|
147
|
+
export async function testProvider(candidate, opts, now = Date.now) {
|
|
148
|
+
const start = now();
|
|
149
|
+
const outcome = await probeModels(candidate, opts);
|
|
150
|
+
if (!outcome.ok)
|
|
151
|
+
return { ok: false, error: outcome.error };
|
|
152
|
+
const latency_ms = Math.max(0, Math.round(now() - start));
|
|
153
|
+
return { ok: true, latency_ms, model_count: outcome.models?.length ?? 0 };
|
|
154
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `providers.json` store — `contracts/data-files/providers.schema.json`,
|
|
3
|
+
* `src/providers.rs:52-86,219-241,673-797`.
|
|
4
|
+
*
|
|
5
|
+
* Security rules enforced here, not by convention:
|
|
6
|
+
* - the file holds PLAINTEXT keys and is written through
|
|
7
|
+
* `OpenOptions::mode(0600)` + fsync + rename, i.e. EVERY save re-asserts
|
|
8
|
+
* 0600 (see `save()`);
|
|
9
|
+
* - `public_view` has NO `api_key` key at all (not null, not empty) — the
|
|
10
|
+
* type returned by every read path simply has no such field, so a handler
|
|
11
|
+
* cannot leak it by accident;
|
|
12
|
+
* - update semantics: `api_key` absent/null/blank KEEPS the stored key (the
|
|
13
|
+
* only keep-on-default field), while `models` absent CLEARS the list,
|
|
14
|
+
* `note` absent clears to "", `request_format` absent silently resets to
|
|
15
|
+
* chat_completions and `name` absent falls back to `id`.
|
|
16
|
+
*/
|
|
17
|
+
import { type StoreResult } from "./result.js";
|
|
18
|
+
export declare const REQUEST_FORMATS: readonly ["chat_completions", "responses", "anthropic_messages"];
|
|
19
|
+
export type RequestFormat = (typeof REQUEST_FORMATS)[number];
|
|
20
|
+
export declare const PROVIDERS_MODE = 384;
|
|
21
|
+
export interface ProviderModel {
|
|
22
|
+
id: string;
|
|
23
|
+
name: string;
|
|
24
|
+
reasoning_efforts: string[];
|
|
25
|
+
context_window: number | null;
|
|
26
|
+
max_output_tokens: number | null;
|
|
27
|
+
/**
|
|
28
|
+
* W804: per-model capability bits. ABSENT = the optimistic default
|
|
29
|
+
* (input ["text","image"], output ["text"]); configuring input ["text"] is the
|
|
30
|
+
* only way to disable the image entry points. Kept optional so re-writing an
|
|
31
|
+
* existing providers.json stays byte-identical until an operator sets them.
|
|
32
|
+
*/
|
|
33
|
+
input_modalities?: string[];
|
|
34
|
+
output_modalities?: string[];
|
|
35
|
+
}
|
|
36
|
+
/** Internal row: the ONLY place an api_key may live. */
|
|
37
|
+
export interface ProviderRow {
|
|
38
|
+
id: string;
|
|
39
|
+
name: string;
|
|
40
|
+
note: string;
|
|
41
|
+
base_url: string;
|
|
42
|
+
request_format: RequestFormat;
|
|
43
|
+
api_key: string | null;
|
|
44
|
+
models: ProviderModel[];
|
|
45
|
+
}
|
|
46
|
+
/** Wire shape: structurally identical to `ProviderRow` MINUS `api_key`. */
|
|
47
|
+
export interface ProviderPublicView {
|
|
48
|
+
id: string;
|
|
49
|
+
name: string;
|
|
50
|
+
note: string;
|
|
51
|
+
base_url: string;
|
|
52
|
+
request_format: RequestFormat;
|
|
53
|
+
models: ProviderModel[];
|
|
54
|
+
is_default: boolean;
|
|
55
|
+
has_key: boolean;
|
|
56
|
+
}
|
|
57
|
+
export interface ProvidersView {
|
|
58
|
+
providers: ProviderPublicView[];
|
|
59
|
+
default_model: string | null;
|
|
60
|
+
}
|
|
61
|
+
export interface ProviderUpsertRequest {
|
|
62
|
+
id?: string;
|
|
63
|
+
name?: string;
|
|
64
|
+
note?: string;
|
|
65
|
+
base_url?: string;
|
|
66
|
+
request_format?: string;
|
|
67
|
+
api_key?: string | null;
|
|
68
|
+
models?: Array<Partial<ProviderModel> & {
|
|
69
|
+
id?: string;
|
|
70
|
+
}>;
|
|
71
|
+
}
|
|
72
|
+
/** Normalize a base_url for the keyless-borrow comparison (trim + no trailing '/'). */
|
|
73
|
+
export declare function normalizeBaseUrl(url: string): string;
|
|
74
|
+
export declare class ProvidersStore {
|
|
75
|
+
private readonly file;
|
|
76
|
+
private data;
|
|
77
|
+
constructor(file: string);
|
|
78
|
+
/** Internal rows — used by the probe and by `/api/config` model listing. */
|
|
79
|
+
rows(): readonly ProviderRow[];
|
|
80
|
+
find(id: string): ProviderRow | undefined;
|
|
81
|
+
defaultModel(): string | null;
|
|
82
|
+
/** Every id listed by any provider (used to validate `default_model`). */
|
|
83
|
+
modelIds(): string[];
|
|
84
|
+
private view;
|
|
85
|
+
viewOf(p: ProviderRow): ProviderPublicView;
|
|
86
|
+
list(): ProviderPublicView[];
|
|
87
|
+
/** GET /api/providers body (no `ok`, never a key). */
|
|
88
|
+
response(): ProvidersView;
|
|
89
|
+
private save;
|
|
90
|
+
private validate;
|
|
91
|
+
/** POST /api/providers — upsert by id; returns the public view. */
|
|
92
|
+
upsert(req: ProviderUpsertRequest): StoreResult<ProviderPublicView>;
|
|
93
|
+
/** POST /api/providers/{id}/delete — UNTRIMMED raw path value. */
|
|
94
|
+
remove(rawId: string): StoreResult<void>;
|
|
95
|
+
/** POST /api/providers/default — persist the default model id. */
|
|
96
|
+
setDefaultModel(model: string): StoreResult<void>;
|
|
97
|
+
}
|