@skaile/forge-settings 1.3.2 → 1.4.0
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/CHANGELOG.md +19 -0
- package/README.md +20 -0
- package/dist/module.json +1 -1
- package/dist/runtime/components/ForgeProvidersPanel.d.vue.ts +13 -0
- package/dist/runtime/components/ForgeProvidersPanel.vue +461 -78
- package/dist/runtime/components/ForgeProvidersPanel.vue.d.ts +13 -0
- package/dist/runtime/server/harness-settings.d.ts +50 -12
- package/dist/runtime/server/harness-settings.js +93 -9
- package/dist/runtime/server/index.d.ts +2 -0
- package/dist/runtime/server/index.js +2 -0
- package/dist/runtime/server/provider-service.d.ts +26 -2
- package/dist/runtime/server/provider-service.js +16 -1
- package/dist/runtime/server/provider-write.d.ts +25 -9
- package/dist/runtime/server/provider-write.js +72 -4
- package/dist/runtime/shared/data-zone.d.ts +66 -0
- package/dist/runtime/shared/data-zone.js +79 -0
- package/dist/runtime/shared/harness-providers.d.ts +41 -2
- package/dist/runtime/shared/harness-providers.js +31 -8
- package/dist/runtime/shared/harnesses.d.ts +7 -0
- package/dist/runtime/shared/harnesses.js +18 -0
- package/dist/runtime/shared/provider-accounts.d.ts +215 -0
- package/dist/runtime/shared/provider-accounts.js +255 -0
- package/dist/runtime/shared/provider-registry.d.ts +38 -0
- package/dist/runtime/shared/provider-registry.js +16 -0
- package/dist/runtime/shared/settings-types.d.ts +7 -0
- package/dist/runtime/vue/index.d.ts +2 -0
- package/dist/runtime/vue/index.js +2 -0
- package/dist/runtime/vue/providers-panel-model.d.ts +98 -5
- package/dist/runtime/vue/providers-panel-model.js +13 -1
- package/dist/runtime/vue/useProvidersPanel.d.ts +66 -5
- package/dist/runtime/vue/useProvidersPanel.js +417 -29
- package/package.json +2 -2
- package/src/runtime/components/ForgeProvidersPanel.vue +477 -78
- package/src/runtime/server/harness-settings.ts +165 -21
- package/src/runtime/server/index.ts +2 -0
- package/src/runtime/server/provider-service.ts +53 -1
- package/src/runtime/server/provider-write.ts +129 -15
- package/src/runtime/shared/data-zone.ts +172 -0
- package/src/runtime/shared/harness-providers.ts +77 -6
- package/src/runtime/shared/harnesses.ts +25 -0
- package/src/runtime/shared/provider-accounts.ts +526 -0
- package/src/runtime/shared/provider-registry.ts +58 -0
- package/src/runtime/shared/settings-types.ts +7 -0
- package/src/runtime/vue/index.ts +2 -0
- package/src/runtime/vue/providers-panel-model.ts +123 -5
- package/src/runtime/vue/useProvidersPanel.ts +586 -37
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { DataZoneRule } from "../shared/provider-registry.js";
|
|
1
2
|
import type { ProvidersPanelClient } from "../vue/providers-client.js";
|
|
2
3
|
import { type ProvidersPanelLayer } from "../vue/useProvidersPanel.js";
|
|
3
4
|
type __VLS_Props = {
|
|
@@ -22,12 +23,24 @@ type __VLS_Props = {
|
|
|
22
23
|
readOnly?: boolean;
|
|
23
24
|
/** The line above the list. Absent: the default text for `layer`; a string replaces it; `false` hides it. */
|
|
24
25
|
note?: string | false;
|
|
26
|
+
/**
|
|
27
|
+
* Expert mode: the sign-in step also offers the raw reference form
|
|
28
|
+
* (`env:` / `keyring:` / `cmd:` / `platform:`), and expert-only
|
|
29
|
+
* parameters (`api_version`) become editable.
|
|
30
|
+
*/
|
|
31
|
+
expert?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* The host's data-zone rule: rows outside it show a muted note naming the
|
|
34
|
+
* rule (`provider-blocked-<id>`). Absent or `any`: none.
|
|
35
|
+
*/
|
|
36
|
+
dataZoneRule?: DataZoneRule;
|
|
25
37
|
};
|
|
26
38
|
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
|
|
27
39
|
changed: () => any;
|
|
28
40
|
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
29
41
|
onChanged?: (() => any) | undefined;
|
|
30
42
|
}>, {
|
|
43
|
+
expert: boolean;
|
|
31
44
|
environmentEnabled: boolean;
|
|
32
45
|
note: string | false;
|
|
33
46
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
@@ -8,12 +8,13 @@
|
|
|
8
8
|
* What lives here, so hosts only read and write their own storage:
|
|
9
9
|
*
|
|
10
10
|
* - {@link validateHarnessSettingsPatch} — the rule set for a harness settings
|
|
11
|
-
* write (`harness`, `harnessModels`, `
|
|
12
|
-
* `lock*`), as a pure function over the request body and
|
|
11
|
+
* write (`harness`, `harnessModels`, `harnessRoles`, `harnessProviders`,
|
|
12
|
+
* `enabledHarnesses`, `lock*`), as a pure function over the request body and
|
|
13
|
+
* the stored state.
|
|
13
14
|
* - {@link resolveLockFlags} — the admin lock flags in force. Locks are
|
|
14
15
|
* `settings.json` fields only; there is no environment override.
|
|
15
|
-
* - {@link applyHarnessLayers} — which harness, model and provider pick
|
|
16
|
-
* session starts with, given the settings layers the host read.
|
|
16
|
+
* - {@link applyHarnessLayers} — which harness, model, roles and provider pick
|
|
17
|
+
* a session starts with, given the settings layers the host read.
|
|
17
18
|
* - {@link enabledHarnessIds} / {@link buildHarnessRows} — the harness catalog
|
|
18
19
|
* `GET /api/agents` serves.
|
|
19
20
|
*
|
|
@@ -21,9 +22,11 @@
|
|
|
21
22
|
*/
|
|
22
23
|
import type { HarnessDetection } from "@skaile/workspaces/sdk/bridge";
|
|
23
24
|
import type { HarnessInfo } from "@skaile/workspaces/types";
|
|
25
|
+
import { type HarnessProviderBlock, type HarnessProviderCandidate } from "../shared/harness-providers.js";
|
|
26
|
+
import type { DataZoneRule } from "../shared/provider-registry.js";
|
|
24
27
|
import type { SkaileSettings } from "../shared/settings-types.js";
|
|
25
28
|
/** The slice of {@link SkaileSettings} that selects, configures and locks harnesses. */
|
|
26
|
-
export type HarnessSettings = Pick<SkaileSettings, "harness" | "harnessModels" | "harnessProviders" | "enabledHarnesses" | "lockHarness" | "lockProvider" | "lockModel" | "lockAudio">;
|
|
29
|
+
export type HarnessSettings = Pick<SkaileSettings, "harness" | "harnessModels" | "harnessRoles" | "harnessProviders" | "enabledHarnesses" | "lockHarness" | "lockProvider" | "lockModel" | "lockAudio">;
|
|
27
30
|
/** One lock flag and where its value comes from. */
|
|
28
31
|
export type LockFlagStatus = {
|
|
29
32
|
value: boolean;
|
|
@@ -59,16 +62,36 @@ export type HarnessRow = HarnessInfo & {
|
|
|
59
62
|
enabled: boolean;
|
|
60
63
|
/** What `detectHarnesses()` found on this host: installed, version, login. */
|
|
61
64
|
detection: HarnessDetection;
|
|
65
|
+
/** The role ids the harness takes ({@link harnessRoleIds}); empty for a one-model harness. */
|
|
66
|
+
roles: string[];
|
|
67
|
+
/** The role → model selector assignments of `s.harnessRoles` for this harness (pass effective settings). */
|
|
68
|
+
roleAssignments: Record<string, string>;
|
|
69
|
+
/**
|
|
70
|
+
* Registry entries on a surface the harness speaks that the host's
|
|
71
|
+
* data-zone rule keeps out, by entry id (reason `blocked-by-rule`, with the
|
|
72
|
+
* message a table shows on the box it cannot tick). Empty without
|
|
73
|
+
* `providers` or under the rule `any`.
|
|
74
|
+
*/
|
|
75
|
+
blockedProviders: Record<string, HarnessProviderBlock>;
|
|
62
76
|
};
|
|
77
|
+
/** The registry entries {@link buildHarnessRows} checks against the host's data-zone rule. */
|
|
78
|
+
export interface HarnessRowProviders {
|
|
79
|
+
/** `GET /api/providers` rows (or any {@link HarnessProviderCandidate}s), in registry order. */
|
|
80
|
+
candidates: readonly HarnessProviderCandidate[];
|
|
81
|
+
/** The host's data-zone rule. Absent: `any`. */
|
|
82
|
+
dataZoneRule?: DataZoneRule;
|
|
83
|
+
}
|
|
63
84
|
/**
|
|
64
85
|
* The rows of `GET /api/agents`: one per harness, in `harnesses` order. Hosts
|
|
65
86
|
* filter to `enabled` unless the request asked for all (`?all=1`).
|
|
66
87
|
* `surfaces` and `providerPlurality` ride along from {@link HarnessInfo} so the
|
|
67
|
-
* Harness tab can offer each harness only the providers it can use
|
|
88
|
+
* Harness tab can offer each harness only the providers it can use; `roles`
|
|
89
|
+
* and `roleAssignments` drive the "Roles" badge and the models-per-task panel.
|
|
68
90
|
* `detections` is the host's `detectHarnesses()` answer; a harness it does not
|
|
69
91
|
* mention (or every harness, when it is omitted) reports `{ status: "unknown" }`.
|
|
92
|
+
* `providers`, when given, fills each row's `blockedProviders`.
|
|
70
93
|
*/
|
|
71
|
-
export declare function buildHarnessRows(harnesses: HarnessInfo[], s: Pick<HarnessSettings, "enabledHarnesses">, detections?: readonly HarnessDetection[]): HarnessRow[];
|
|
94
|
+
export declare function buildHarnessRows(harnesses: HarnessInfo[], s: Pick<HarnessSettings, "enabledHarnesses" | "harnessRoles">, detections?: readonly HarnessDetection[], providers?: HarnessRowProviders): HarnessRow[];
|
|
72
95
|
/** What {@link validateHarnessSettingsPatch} hands back for a valid body. */
|
|
73
96
|
export type HarnessSettingsPatch = {
|
|
74
97
|
/**
|
|
@@ -77,9 +100,9 @@ export type HarnessSettingsPatch = {
|
|
|
77
100
|
*/
|
|
78
101
|
patch: Partial<HarnessSettings>;
|
|
79
102
|
/**
|
|
80
|
-
* Whether the effective `harness`, `harnessModels`, `
|
|
81
|
-
* `enabledHarnesses` changed, i.e. whether running
|
|
82
|
-
* defaults and the host should stop them.
|
|
103
|
+
* Whether the effective `harness`, `harnessModels`, `harnessRoles`,
|
|
104
|
+
* `harnessProviders` or `enabledHarnesses` changed, i.e. whether running
|
|
105
|
+
* sessions now run on stale defaults and the host should stop them.
|
|
83
106
|
*/
|
|
84
107
|
sessionAffecting: boolean;
|
|
85
108
|
};
|
|
@@ -94,6 +117,11 @@ export type HarnessSettingsPatch = {
|
|
|
94
117
|
* keys are present `harness` wins.
|
|
95
118
|
* - `harnessModels`: patched per harness id (catalog ids only); a value is a
|
|
96
119
|
* `providerId/modelId` selector, an empty string removes that harness's entry.
|
|
120
|
+
* - `harnessRoles`: patched per harness id, then per role. A harness must be in
|
|
121
|
+
* the catalog and take roles ({@link harnessRoleIds}); a role must be one of
|
|
122
|
+
* its roles; a value is a `providerId/modelId` selector, and one of
|
|
123
|
+
* `ctx.runnableModels` when that list is given. An empty string or `null`
|
|
124
|
+
* removes the role; `{}` or `null` for a harness removes all its roles.
|
|
97
125
|
* - `harnessProviders`: the registry entry ids picked per harness id. The modal
|
|
98
126
|
* sends the whole map, so it replaces the stored one: each value must be an
|
|
99
127
|
* array of strings (empty strings are dropped, duplicates collapse, order is
|
|
@@ -114,6 +142,8 @@ export type HarnessSettingsPatch = {
|
|
|
114
142
|
* @param ctx.catalog valid harness ids (`listHarnesses().map(d => d.id)`).
|
|
115
143
|
* @param ctx.current the stored settings the patch applies to (the instance file).
|
|
116
144
|
* @param ctx.locks the lock flags in force ({@link resolveLockFlags} of `current`).
|
|
145
|
+
* @param ctx.runnableModels optional: the selectors a session may run; when
|
|
146
|
+
* given, a `harnessRoles` value outside it is refused.
|
|
117
147
|
* @returns the patch, or the error message of the first violated rule (a
|
|
118
148
|
* string, mirroring `validateEnvSettingsUpdate`; hosts answer 400 with it).
|
|
119
149
|
*/
|
|
@@ -121,18 +151,21 @@ export declare function validateHarnessSettingsPatch(body: unknown, ctx: {
|
|
|
121
151
|
catalog: Iterable<string>;
|
|
122
152
|
current: HarnessSettings;
|
|
123
153
|
locks: LockFlags;
|
|
154
|
+
runnableModels?: Iterable<string>;
|
|
124
155
|
}): HarnessSettingsPatch | string;
|
|
125
|
-
/** One settings layer's say in which harness, model and provider pick a session gets. */
|
|
156
|
+
/** One settings layer's say in which harness, model, roles and provider pick a session gets. */
|
|
126
157
|
export type HarnessLayer = {
|
|
127
158
|
/** The harness id (the workspaces settings key `harness`). */
|
|
128
159
|
harness?: string;
|
|
129
160
|
/** An explicit model selector; wins over this layer's `harnessModels` entry. */
|
|
130
161
|
model?: string;
|
|
131
162
|
harnessModels?: Record<string, string>;
|
|
163
|
+
/** Role → model selector per harness id; layered per role. */
|
|
164
|
+
harnessRoles?: Record<string, Record<string, string>>;
|
|
132
165
|
harnessProviders?: Record<string, string[]>;
|
|
133
166
|
};
|
|
134
167
|
/**
|
|
135
|
-
* Resolve the harness, model and provider pick a session starts with.
|
|
168
|
+
* Resolve the harness, model, roles and provider pick a session starts with.
|
|
136
169
|
*
|
|
137
170
|
* `layers[0]` is the instance layer; later layers (workspace file, workspace
|
|
138
171
|
* overrides, chat, …) are more specific and override it per field — the last
|
|
@@ -146,10 +179,15 @@ export type HarnessLayer = {
|
|
|
146
179
|
* `layer.harnessProviders[harness]` — both keyed by the *resolved* harness, so
|
|
147
180
|
* a pick stored for another harness never applies.
|
|
148
181
|
*
|
|
182
|
+
* `roles` is layered per role of the resolved harness: for each role the last
|
|
183
|
+
* layer with a non-empty selector wins; `locks.model` freezes it to layer 0's
|
|
184
|
+
* roles, as it freezes `model`. No role set anywhere → `undefined`.
|
|
185
|
+
*
|
|
149
186
|
* Fields no layer sets come back `undefined`; the host supplies its defaults.
|
|
150
187
|
*/
|
|
151
188
|
export declare function applyHarnessLayers(layers: HarnessLayer[], locks: LockFlags): {
|
|
152
189
|
harness: string | undefined;
|
|
153
190
|
model: string | undefined;
|
|
191
|
+
roles: Record<string, string> | undefined;
|
|
154
192
|
providers: string[] | undefined;
|
|
155
193
|
};
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import {
|
|
2
|
+
harnessProviderBlock
|
|
3
|
+
} from "../shared/harness-providers.js";
|
|
1
4
|
import {
|
|
2
5
|
DEFAULT_ENABLED_HARNESSES,
|
|
3
6
|
HARNESS_LABELS,
|
|
7
|
+
harnessRoleIds,
|
|
4
8
|
isExperimentalHarness
|
|
5
9
|
} from "../shared/harnesses.js";
|
|
6
10
|
const LOCK_FIELDS = [
|
|
@@ -21,15 +25,29 @@ export function resolveLockFlags(stored) {
|
|
|
21
25
|
export function enabledHarnessIds(s) {
|
|
22
26
|
return s.enabledHarnesses?.length ? s.enabledHarnesses : [...DEFAULT_ENABLED_HARNESSES];
|
|
23
27
|
}
|
|
24
|
-
export function buildHarnessRows(harnesses, s, detections = []) {
|
|
28
|
+
export function buildHarnessRows(harnesses, s, detections = [], providers) {
|
|
25
29
|
const enabled = new Set(enabledHarnessIds(s));
|
|
26
|
-
return harnesses.map((info) =>
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
return harnesses.map((info) => {
|
|
31
|
+
const roles = harnessRoleIds(info.id);
|
|
32
|
+
const stored = stringEntries(s.harnessRoles?.[info.id]);
|
|
33
|
+
const roleAssignments = {};
|
|
34
|
+
for (const role of roles) if (stored[role]) roleAssignments[role] = stored[role];
|
|
35
|
+
const blockedProviders = {};
|
|
36
|
+
for (const candidate of providers?.candidates ?? []) {
|
|
37
|
+
const block = harnessProviderBlock(info, candidate, providers?.dataZoneRule);
|
|
38
|
+
if (block) blockedProviders[candidate.id] = block;
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
...info,
|
|
42
|
+
label: HARNESS_LABELS[info.id] ?? info.name,
|
|
43
|
+
experimental: isExperimentalHarness(info.id),
|
|
44
|
+
enabled: enabled.has(info.id),
|
|
45
|
+
detection: detections.find((d) => d.id === info.id) ?? { id: info.id, status: "unknown" },
|
|
46
|
+
roles,
|
|
47
|
+
roleAssignments,
|
|
48
|
+
blockedProviders
|
|
49
|
+
};
|
|
50
|
+
});
|
|
33
51
|
}
|
|
34
52
|
const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
35
53
|
const nonEmpty = (value) => typeof value === "string" && value ? value : void 0;
|
|
@@ -66,6 +84,19 @@ function sameIdLists(a, b) {
|
|
|
66
84
|
(key) => b[key] !== void 0 && a[key].length === b[key].length && a[key].every((id, i) => id === b[key][i])
|
|
67
85
|
);
|
|
68
86
|
}
|
|
87
|
+
function roleMaps(value) {
|
|
88
|
+
const maps = {};
|
|
89
|
+
if (!isObject(value)) return maps;
|
|
90
|
+
for (const [harnessId, roles] of Object.entries(value)) {
|
|
91
|
+
const entries = stringEntries(roles);
|
|
92
|
+
if (Object.keys(entries).length) maps[harnessId] = entries;
|
|
93
|
+
}
|
|
94
|
+
return maps;
|
|
95
|
+
}
|
|
96
|
+
function sameRoleMaps(a, b) {
|
|
97
|
+
const keys = Object.keys(a);
|
|
98
|
+
return keys.length === Object.keys(b).length && keys.every((key) => b[key] !== void 0 && sameEntries(a[key], b[key]));
|
|
99
|
+
}
|
|
69
100
|
export function validateHarnessSettingsPatch(body, ctx) {
|
|
70
101
|
if (!isObject(body)) return "body must be an object";
|
|
71
102
|
if ("model" in body) return "model is set per harness - use harnessModels";
|
|
@@ -120,6 +151,51 @@ export function validateHarnessSettingsPatch(body, ctx) {
|
|
|
120
151
|
sessionAffecting = true;
|
|
121
152
|
}
|
|
122
153
|
}
|
|
154
|
+
if ("harnessRoles" in body) {
|
|
155
|
+
const raw = body.harnessRoles;
|
|
156
|
+
if (!isObject(raw)) return "harnessRoles must be an object";
|
|
157
|
+
const runnable = ctx.runnableModels ? new Set(ctx.runnableModels) : void 0;
|
|
158
|
+
const currentRoles = roleMaps(current.harnessRoles);
|
|
159
|
+
const nextRoles = {};
|
|
160
|
+
for (const [id, roles] of Object.entries(currentRoles)) nextRoles[id] = { ...roles };
|
|
161
|
+
for (const [id, roles] of Object.entries(raw)) {
|
|
162
|
+
if (!catalog.has(id)) return `harnessRoles: unknown harness "${id}"`;
|
|
163
|
+
const supported = harnessRoleIds(id);
|
|
164
|
+
if (!supported.length)
|
|
165
|
+
return `harnessRoles: harness "${id}" runs one model and takes no roles`;
|
|
166
|
+
if (roles === null) {
|
|
167
|
+
delete nextRoles[id];
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (!isObject(roles)) return `harnessRoles.${id} must be an object`;
|
|
171
|
+
if (!Object.keys(roles).length) {
|
|
172
|
+
delete nextRoles[id];
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
const next = { ...nextRoles[id] };
|
|
176
|
+
for (const [role, entry] of Object.entries(roles)) {
|
|
177
|
+
if (!supported.includes(role)) return `harnessRoles.${id}: unknown role "${role}"`;
|
|
178
|
+
if (entry !== null && typeof entry !== "string")
|
|
179
|
+
return `harnessRoles.${id}.${role} must be a string`;
|
|
180
|
+
const value = typeof entry === "string" ? entry.trim() : "";
|
|
181
|
+
if (!value) {
|
|
182
|
+
delete next[role];
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (!isModelSelector(value))
|
|
186
|
+
return `harnessRoles.${id}.${role} must be a "providerId/modelId" selector`;
|
|
187
|
+
if (runnable && !runnable.has(value))
|
|
188
|
+
return `harnessRoles.${id}.${role}: model "${value}" is not runnable`;
|
|
189
|
+
next[role] = value;
|
|
190
|
+
}
|
|
191
|
+
if (Object.keys(next).length) nextRoles[id] = next;
|
|
192
|
+
else delete nextRoles[id];
|
|
193
|
+
}
|
|
194
|
+
if (!sameRoleMaps(currentRoles, nextRoles)) {
|
|
195
|
+
patch.harnessRoles = Object.keys(nextRoles).length ? nextRoles : void 0;
|
|
196
|
+
sessionAffecting = true;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
123
199
|
if ("enabledHarnesses" in body) {
|
|
124
200
|
const raw = body.enabledHarnesses;
|
|
125
201
|
if (!Array.isArray(raw) || !raw.every((id) => typeof id === "string")) {
|
|
@@ -163,5 +239,13 @@ export function applyHarnessLayers(layers, locks) {
|
|
|
163
239
|
const ids = harness === void 0 ? void 0 : layer.harnessProviders?.[harness];
|
|
164
240
|
return ids?.length ? ids : void 0;
|
|
165
241
|
});
|
|
166
|
-
|
|
242
|
+
let roles;
|
|
243
|
+
if (harness !== void 0) {
|
|
244
|
+
const merged = {};
|
|
245
|
+
const roleLayers = locks.model.value ? layers.slice(0, 1) : layers;
|
|
246
|
+
for (const layer of roleLayers)
|
|
247
|
+
Object.assign(merged, stringEntries(layer.harnessRoles?.[harness]));
|
|
248
|
+
if (Object.keys(merged).length) roles = merged;
|
|
249
|
+
}
|
|
250
|
+
return { harness, model, roles, providers };
|
|
167
251
|
}
|
|
@@ -6,9 +6,11 @@
|
|
|
6
6
|
* Imports no Vue, no editor, no AI SDK and no agent session code. Storage,
|
|
7
7
|
* authentication and workspace permissions stay with the host.
|
|
8
8
|
*/
|
|
9
|
+
export * from "../shared/data-zone.js";
|
|
9
10
|
export * from "../shared/harness-providers.js";
|
|
10
11
|
export * from "../shared/harnesses.js";
|
|
11
12
|
export * from "../shared/model-roles.js";
|
|
13
|
+
export * from "../shared/provider-accounts.js";
|
|
12
14
|
export * from "../shared/provider-registry.js";
|
|
13
15
|
export type { DomainConfig, SkaileSettings } from "../shared/settings-types.js";
|
|
14
16
|
export * from "./credential-policy.js";
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
export * from "../shared/data-zone.js";
|
|
1
2
|
export * from "../shared/harness-providers.js";
|
|
2
3
|
export * from "../shared/harnesses.js";
|
|
3
4
|
export * from "../shared/model-roles.js";
|
|
5
|
+
export * from "../shared/provider-accounts.js";
|
|
4
6
|
export * from "../shared/provider-registry.js";
|
|
5
7
|
export * from "./credential-policy.js";
|
|
6
8
|
export * from "./credential-store.js";
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* `model_map`/`capabilities`; harness-backed discovery is
|
|
19
19
|
* `listModelsForHarness(harnessId, registry)` in the runtime.
|
|
20
20
|
*/
|
|
21
|
-
import type { AIProviderCapabilities, AIProviderCredentialKind, AIProviderKind, AIProviderRegistryView, AIProviderSurface, AIProviderVoiceParams } from "../shared/provider-registry.js";
|
|
21
|
+
import type { AIProviderCapabilities, AIProviderCredentialKind, AIProviderKind, AIProviderRegistryView, AIProviderSurface, AIProviderTransport, AIProviderVoiceParams, DataZone } from "../shared/provider-registry.js";
|
|
22
22
|
import { type CredentialSource, type CredentialStoreOptions } from "./credential-store.js";
|
|
23
23
|
/** One registry entry, as a settings UI needs to see it. */
|
|
24
24
|
export interface ProviderInfo {
|
|
@@ -52,6 +52,14 @@ export interface ProviderInfo {
|
|
|
52
52
|
preset?: string;
|
|
53
53
|
/** Wire protocol of an `llm`/`embed` entry (its own or its preset's). */
|
|
54
54
|
surface?: AIProviderSurface;
|
|
55
|
+
/** How requests of an `llm`/`embed` entry reach its surface, when not `direct`. */
|
|
56
|
+
transport?: AIProviderTransport;
|
|
57
|
+
/**
|
|
58
|
+
* Routing parameters of an `llm`/`embed` entry (`resource`, `deployment`,
|
|
59
|
+
* `api_version`, `region`, `project`, `location`, …), when it has any.
|
|
60
|
+
* Voice entries carry theirs in {@link params}.
|
|
61
|
+
*/
|
|
62
|
+
transportParams?: Record<string, string>;
|
|
55
63
|
/** The entry's `enabled` flag; `false` only in a listing with `includeDisabled`. */
|
|
56
64
|
enabled: boolean;
|
|
57
65
|
/**
|
|
@@ -63,6 +71,21 @@ export interface ProviderInfo {
|
|
|
63
71
|
params?: AIProviderVoiceParams;
|
|
64
72
|
/** Frozen by the control plane: a lower layer cannot change or disable it. */
|
|
65
73
|
locked: boolean;
|
|
74
|
+
/** Where the entry processes data: its `data_zone`, else its preset's, else `transfer`. */
|
|
75
|
+
dataZone: DataZone;
|
|
76
|
+
/** The entry's `eu_processing` switch (an `eu_option` entry counts as EU only with it on). */
|
|
77
|
+
euProcessing: boolean;
|
|
78
|
+
/**
|
|
79
|
+
* The entry's `allowed_models`, when it sets one: the only models it may be
|
|
80
|
+
* used with (`[]` = none). Absent: every model.
|
|
81
|
+
*/
|
|
82
|
+
allowedModels?: string[];
|
|
83
|
+
/**
|
|
84
|
+
* Logical model ids an `llm`/`embed` entry declares (`model_map` keys, then
|
|
85
|
+
* ids only `capabilities` names), when it declares any. An entry without
|
|
86
|
+
* them lists its models through discovery.
|
|
87
|
+
*/
|
|
88
|
+
modelIds?: string[];
|
|
66
89
|
}
|
|
67
90
|
/** One selectable model of one provider. */
|
|
68
91
|
export interface ModelInfo {
|
|
@@ -110,7 +133,8 @@ export declare function listAIProviders(registry: AIProviderRegistryView, option
|
|
|
110
133
|
*
|
|
111
134
|
* Sourced from each `llm`/`embed` entry's `model_map` (plus any id that only
|
|
112
135
|
* appears in `capabilities`) — the same two tables the runtime's model-reference
|
|
113
|
-
* resolver reads, so a selector built from this list always resolves.
|
|
136
|
+
* resolver reads, so a selector built from this list always resolves. A model
|
|
137
|
+
* outside the entry's `allowed_models` is left out, as the runtime refuses it.
|
|
114
138
|
*
|
|
115
139
|
* @param providerId - Restrict to one entry; omit for every enabled entry.
|
|
116
140
|
*/
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { entryDataZone } from "../shared/data-zone.js";
|
|
1
2
|
import {
|
|
2
3
|
entriesOfKind,
|
|
3
4
|
isEntryEnabled,
|
|
5
|
+
isModelAllowed,
|
|
4
6
|
isVoiceEntry,
|
|
5
7
|
parseCredentialRef,
|
|
6
8
|
registryEntries
|
|
@@ -32,13 +34,25 @@ export function listAIProviders(registry, options = {}) {
|
|
|
32
34
|
...entry.base_url ? { baseUrl: entry.base_url } : {},
|
|
33
35
|
...entry.preset ? { preset: entry.preset } : {},
|
|
34
36
|
...!isVoiceEntry(entry) && entry.surface ? { surface: entry.surface } : {},
|
|
37
|
+
...!isVoiceEntry(entry) && entry.transport && entry.transport !== "direct" ? { transport: entry.transport } : {},
|
|
38
|
+
...!isVoiceEntry(entry) && entry.params && Object.keys(entry.params).length > 0 ? { transportParams: { ...entry.params } } : {},
|
|
35
39
|
enabled,
|
|
36
40
|
...isVoiceEntry(entry) && entry.params ? { params: entry.params } : {},
|
|
37
|
-
locked: entry.locked === true
|
|
41
|
+
locked: entry.locked === true,
|
|
42
|
+
dataZone: entryDataZone(entry),
|
|
43
|
+
euProcessing: entry.eu_processing === true,
|
|
44
|
+
...!isVoiceEntry(entry) && entry.allowed_models ? { allowedModels: [...entry.allowed_models] } : {},
|
|
45
|
+
...!isVoiceEntry(entry) ? declaredModelIds(entry) : {}
|
|
38
46
|
});
|
|
39
47
|
}
|
|
40
48
|
return providers;
|
|
41
49
|
}
|
|
50
|
+
function declaredModelIds(entry) {
|
|
51
|
+
const ids = [
|
|
52
|
+
.../* @__PURE__ */ new Set([...Object.keys(entry.model_map ?? {}), ...Object.keys(entry.capabilities ?? {})])
|
|
53
|
+
];
|
|
54
|
+
return ids.length > 0 ? { modelIds: ids } : {};
|
|
55
|
+
}
|
|
42
56
|
export function listAIProviderModels(registry, providerId) {
|
|
43
57
|
const models = [];
|
|
44
58
|
for (const kind of ["llm", "embed"]) {
|
|
@@ -49,6 +63,7 @@ export function listAIProviderModels(registry, providerId) {
|
|
|
49
63
|
...Object.keys(entry.capabilities ?? {})
|
|
50
64
|
]);
|
|
51
65
|
for (const modelId of logicalIds) {
|
|
66
|
+
if (!isModelAllowed(entry, modelId)) continue;
|
|
52
67
|
const capabilities = entry.capabilities?.[modelId];
|
|
53
68
|
models.push({
|
|
54
69
|
id: `${id}/${modelId}`,
|
|
@@ -74,8 +74,11 @@ export declare function providerScopeWritable(scope: ProviderWriteScope, caller:
|
|
|
74
74
|
* Upsert entry `id` in the scope's document.
|
|
75
75
|
*
|
|
76
76
|
* - **Own entry** — the body sets the editable fields (`BODY_KEYS`) — one it
|
|
77
|
-
* omits is removed — and optionally `kind
|
|
78
|
-
* `
|
|
77
|
+
* omits is removed — and optionally `kind`, `params`, `data_zone`,
|
|
78
|
+
* `eu_processing` and `allowed_models`; fields it does not set (`kind`,
|
|
79
|
+
* `transport`, `params`, `data_zone`, …) keep their written value,
|
|
80
|
+
* `params: {}` removes the parameters and `null` removes `data_zone`,
|
|
81
|
+
* `eu_processing` or `allowed_models` (`[]` allows no model).
|
|
79
82
|
* - **Patch** — when a lower document defines `id` and the body holds only
|
|
80
83
|
* {@link PATCH_KEYS}, the body is merged into the scope's stored entry for
|
|
81
84
|
* `id` (created when absent), leaving every other stored field alone.
|
|
@@ -83,11 +86,16 @@ export declare function providerScopeWritable(scope: ProviderWriteScope, caller:
|
|
|
83
86
|
* The file and its directory are created when absent; other entries are kept
|
|
84
87
|
* as written.
|
|
85
88
|
*
|
|
86
|
-
* 400: invalid id, secret-bearing or malformed body
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
89
|
+
* 400: invalid id, secret-bearing or malformed body (`data_zone` outside
|
|
90
|
+
* eu | eu_option | transfer | local, a non-boolean `eu_processing`, an
|
|
91
|
+
* `allowed_models` that is not a list of distinct non-empty ids), a body for
|
|
92
|
+
* an inherited entry that is neither a patch nor over an own full entry, a
|
|
93
|
+
* preset's required parameter left out of `params` (named in the message,
|
|
94
|
+
* `data.missingParams`), or any error diagnostic for the entry (as
|
|
95
|
+
* `data.diagnostics`; nothing is written — `params` keys a transport does not
|
|
96
|
+
* accept are refused this way, by the SDK's schema). 403: the entry is
|
|
97
|
+
* `locked: true` in some document. 409: the document on disk is not a
|
|
98
|
+
* registry.
|
|
91
99
|
*/
|
|
92
100
|
export declare function upsertProviderEntry(scope: ProviderWriteScope, id: string, body: unknown): {
|
|
93
101
|
id: string;
|
|
@@ -107,12 +115,18 @@ export interface ProviderPresetSummary {
|
|
|
107
115
|
label: string;
|
|
108
116
|
kind: AIProviderPreset["kind"];
|
|
109
117
|
surface?: AIProviderPreset["surface"];
|
|
118
|
+
/** How requests reach the surface; absent = `direct`. Azure transports offer Entra ID sign-in. */
|
|
119
|
+
transport?: AIProviderPreset["transport"];
|
|
110
120
|
base_url?: string;
|
|
111
121
|
base_url_env?: string;
|
|
112
122
|
credential: {
|
|
113
123
|
kind: AIProviderPreset["credential"]["kind"];
|
|
114
124
|
env?: string;
|
|
115
125
|
};
|
|
126
|
+
/** Default routing / voice parameters. */
|
|
127
|
+
params?: Record<string, string>;
|
|
128
|
+
/** `params` keys an entry naming this preset must supply. */
|
|
129
|
+
required_params?: string[];
|
|
116
130
|
}
|
|
117
131
|
/** Catalog presets (all of them by default), in catalog order. */
|
|
118
132
|
export declare function providerPresetSummaries(presets?: AIProviderPreset[]): ProviderPresetSummary[];
|
|
@@ -129,14 +143,16 @@ export declare function detectedProviderPresets(env: Record<string, string | und
|
|
|
129
143
|
export type ProviderLayerName = ProviderWriteScope["name"];
|
|
130
144
|
/**
|
|
131
145
|
* The defining document's values for the `PUT`-editable fields a settings UI
|
|
132
|
-
* edit dialog does not show
|
|
133
|
-
* so an edit sends these back
|
|
146
|
+
* edit dialog does not show (or shows only in part, as `params`). A `PUT`
|
|
147
|
+
* removes every editable field it omits, so an edit sends these back
|
|
148
|
+
* unchanged instead of dropping them.
|
|
134
149
|
*/
|
|
135
150
|
export interface ProviderLayerFields {
|
|
136
151
|
label?: unknown;
|
|
137
152
|
headers?: unknown;
|
|
138
153
|
model_map?: unknown;
|
|
139
154
|
enabled?: unknown;
|
|
155
|
+
params?: unknown;
|
|
140
156
|
}
|
|
141
157
|
/**
|
|
142
158
|
* Tag each provider view of a scope with every registry document that holds
|
|
@@ -10,7 +10,9 @@ import {
|
|
|
10
10
|
AI_PROVIDER_PRESETS,
|
|
11
11
|
expandPreset
|
|
12
12
|
} from "@skaile/workspaces/sdk/bridge";
|
|
13
|
+
import { isDataZone } from "../shared/data-zone.js";
|
|
13
14
|
import { isPlainObject } from "../shared/guards.js";
|
|
15
|
+
import { missingRequiredParams } from "../shared/provider-accounts.js";
|
|
14
16
|
import { entryPolicyRefusal } from "./credential-policy.js";
|
|
15
17
|
import { catalogPreset, readProviderLayer, writeProviderLayer } from "./provider-layers.js";
|
|
16
18
|
export class ProviderWriteError extends Error {
|
|
@@ -41,6 +43,26 @@ const BODY_KEYS = {
|
|
|
41
43
|
model_map: true,
|
|
42
44
|
enabled: true
|
|
43
45
|
};
|
|
46
|
+
const KEPT_BODY_KEYS = {
|
|
47
|
+
kind: true,
|
|
48
|
+
params: true,
|
|
49
|
+
data_zone: true,
|
|
50
|
+
eu_processing: true,
|
|
51
|
+
allowed_models: true
|
|
52
|
+
};
|
|
53
|
+
const NULLABLE_BODY_KEYS = ["data_zone", "eu_processing", "allowed_models"];
|
|
54
|
+
function readAllowedModels(value) {
|
|
55
|
+
const refusal = "allowed_models must be a list of distinct, non-empty model ids, or null";
|
|
56
|
+
if (!Array.isArray(value)) return refusal;
|
|
57
|
+
const ids = [];
|
|
58
|
+
for (const item of value) {
|
|
59
|
+
if (typeof item !== "string" || item.trim() === "") return refusal;
|
|
60
|
+
const id = item.trim();
|
|
61
|
+
if (ids.includes(id)) return `allowed_models lists '${id}' twice`;
|
|
62
|
+
ids.push(id);
|
|
63
|
+
}
|
|
64
|
+
return ids;
|
|
65
|
+
}
|
|
44
66
|
export const PATCH_KEYS = ["credential", "enabled"];
|
|
45
67
|
const SECRET_KEYS = { apiKey: true, api_key: true, token: true };
|
|
46
68
|
export function providerScopeWritable(scope, caller) {
|
|
@@ -74,8 +96,25 @@ function readEntryBody(body) {
|
|
|
74
96
|
if (Object.keys(body).some((key) => Object.hasOwn(SECRET_KEYS, key)) || credentialKeys.some((key) => key === "value" || Object.hasOwn(SECRET_KEYS, key))) {
|
|
75
97
|
return PROVIDER_SECRET_REFUSAL;
|
|
76
98
|
}
|
|
77
|
-
const unknown = Object.keys(body).find(
|
|
99
|
+
const unknown = Object.keys(body).find(
|
|
100
|
+
(key) => !Object.hasOwn(BODY_KEYS, key) && !Object.hasOwn(KEPT_BODY_KEYS, key)
|
|
101
|
+
);
|
|
78
102
|
if (unknown) return `'${unknown}' is not a provider entry field`;
|
|
103
|
+
if (body.params !== void 0 && !isPlainObject(body.params)) {
|
|
104
|
+
return "params must be a map of parameter name to value";
|
|
105
|
+
}
|
|
106
|
+
if (body.data_zone !== void 0 && body.data_zone !== null && !isDataZone(body.data_zone)) {
|
|
107
|
+
return "data_zone must be one of eu, eu_option, transfer, local, or null";
|
|
108
|
+
}
|
|
109
|
+
if (body.eu_processing !== void 0 && body.eu_processing !== null && typeof body.eu_processing !== "boolean") {
|
|
110
|
+
return "eu_processing must be true, false or null";
|
|
111
|
+
}
|
|
112
|
+
let allowedModels;
|
|
113
|
+
if (body.allowed_models !== void 0 && body.allowed_models !== null) {
|
|
114
|
+
const read = readAllowedModels(body.allowed_models);
|
|
115
|
+
if (typeof read === "string") return read;
|
|
116
|
+
allowedModels = read;
|
|
117
|
+
}
|
|
79
118
|
if (body.preset !== void 0 && !catalogPreset(body.preset)) {
|
|
80
119
|
return `Unknown preset '${String(body.preset)}'`;
|
|
81
120
|
}
|
|
@@ -85,7 +124,7 @@ function readEntryBody(body) {
|
|
|
85
124
|
if (ref !== void 0 && (typeof ref !== "string" || !parseCredentialRef(ref)))
|
|
86
125
|
return CREDENTIAL_REF_REFUSAL;
|
|
87
126
|
}
|
|
88
|
-
return body;
|
|
127
|
+
return allowedModels ? { ...body, allowed_models: allowedModels } : body;
|
|
89
128
|
}
|
|
90
129
|
function entryErrors(scope, providers, id) {
|
|
91
130
|
const layer = mergeLayerOf(scope);
|
|
@@ -118,6 +157,22 @@ function isPatchOrAbsent(entry) {
|
|
|
118
157
|
if (entry === void 0) return true;
|
|
119
158
|
return isPlainObject(entry) && Object.keys(entry).every((key) => PATCH_KEYS.includes(key));
|
|
120
159
|
}
|
|
160
|
+
function lowerPreset(lowerEntries) {
|
|
161
|
+
let preset;
|
|
162
|
+
for (const entry of lowerEntries)
|
|
163
|
+
if (isPlainObject(entry) && entry.preset !== void 0) preset = entry.preset;
|
|
164
|
+
return preset;
|
|
165
|
+
}
|
|
166
|
+
function missingPresetParams(entry, lowerEntries) {
|
|
167
|
+
const preset = catalogPreset(entry.preset ?? lowerPreset(lowerEntries));
|
|
168
|
+
if (!preset) return [];
|
|
169
|
+
const params = {};
|
|
170
|
+
for (const lower of lowerEntries) {
|
|
171
|
+
if (isPlainObject(lower) && isPlainObject(lower.params)) Object.assign(params, lower.params);
|
|
172
|
+
}
|
|
173
|
+
if (isPlainObject(entry.params)) Object.assign(params, entry.params);
|
|
174
|
+
return missingRequiredParams(preset, params);
|
|
175
|
+
}
|
|
121
176
|
export function upsertProviderEntry(scope, id, body) {
|
|
122
177
|
if (!isSafeProviderId(id)) {
|
|
123
178
|
throw createError({
|
|
@@ -150,6 +205,16 @@ export function upsertProviderEntry(scope, id, body) {
|
|
|
150
205
|
Object.entries(existing).filter(([key]) => !Object.hasOwn(BODY_KEYS, key))
|
|
151
206
|
) : {};
|
|
152
207
|
entry = { ...kept, ...fields };
|
|
208
|
+
if (isPlainObject(entry.params) && Object.keys(entry.params).length === 0) delete entry.params;
|
|
209
|
+
for (const key of NULLABLE_BODY_KEYS) if (entry[key] === null) delete entry[key];
|
|
210
|
+
const missing = missingPresetParams(entry, lowerEntries);
|
|
211
|
+
if (missing.length > 0) {
|
|
212
|
+
throw createError({
|
|
213
|
+
statusCode: 400,
|
|
214
|
+
statusMessage: `Provider '${id}' needs ${missing.map((key) => `params.${key}`).join(", ")} (required by preset '${String(entry.preset ?? lowerPreset(lowerEntries))}')`,
|
|
215
|
+
data: { missingParams: missing }
|
|
216
|
+
});
|
|
217
|
+
}
|
|
153
218
|
}
|
|
154
219
|
const next = { ...providers, [id]: entry };
|
|
155
220
|
const refusal = entryPolicyRefusal(entry, scope.policy);
|
|
@@ -183,9 +248,12 @@ export function providerPresetSummaries(presets = Object.values(AI_PROVIDER_PRES
|
|
|
183
248
|
label: preset.label,
|
|
184
249
|
kind: preset.kind,
|
|
185
250
|
surface: preset.surface,
|
|
251
|
+
...preset.transport ? { transport: preset.transport } : {},
|
|
186
252
|
base_url: preset.base_url,
|
|
187
253
|
base_url_env: preset.base_url_env,
|
|
188
|
-
credential: { kind: preset.credential.kind, env: preset.credential.env }
|
|
254
|
+
credential: { kind: preset.credential.kind, env: preset.credential.env },
|
|
255
|
+
...preset.params ? { params: { ...preset.params } } : {},
|
|
256
|
+
...preset.required_params?.length ? { required_params: [...preset.required_params] } : {}
|
|
189
257
|
}));
|
|
190
258
|
}
|
|
191
259
|
export function detectedProviderPresets(env, registry) {
|
|
@@ -200,7 +268,7 @@ export function detectedProviderPresets(env, registry) {
|
|
|
200
268
|
})
|
|
201
269
|
);
|
|
202
270
|
}
|
|
203
|
-
const LAYER_FIELD_KEYS = ["label", "headers", "model_map", "enabled"];
|
|
271
|
+
const LAYER_FIELD_KEYS = ["label", "headers", "model_map", "enabled", "params"];
|
|
204
272
|
export function withProviderLayers(views, scope) {
|
|
205
273
|
const own = readLayerLeniently(scope.file);
|
|
206
274
|
const lower = scope.below.map((file) => readLayerLeniently(file));
|