@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
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data zones: where a provider entry processes data, and which entries a
|
|
3
|
+
* host's data-zone rule ("Where may AI process data?") keeps.
|
|
4
|
+
*
|
|
5
|
+
* `@skaile/workspaces` 9.1 owns this (`entryDataZone`,
|
|
6
|
+
* `entryAllowedByDataZone`, `filterRegistryByDataZone` in
|
|
7
|
+
* `bridge/src/provider-catalog.ts`, the `data_zone` of each catalog preset).
|
|
8
|
+
* forge-settings mirrors it here so the settings panel and a host on an older
|
|
9
|
+
* SDK apply the same rule in the browser and on the server. Semantics match
|
|
10
|
+
* upstream exactly; {@link PRESET_DATA_ZONES} is a copy of the catalog's
|
|
11
|
+
* column, and the SDK catalog is its source.
|
|
12
|
+
*
|
|
13
|
+
* Browser-safe on purpose — no `node:` imports.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
type AIProviderRegistry,
|
|
18
|
+
type AIProviderRegistryView,
|
|
19
|
+
DATA_ZONE_RULES,
|
|
20
|
+
DATA_ZONES,
|
|
21
|
+
type DataZone,
|
|
22
|
+
type DataZoneRule,
|
|
23
|
+
isMergedRegistry,
|
|
24
|
+
type MergedAIProviderRegistry,
|
|
25
|
+
} from "./provider-registry";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The `data_zone` of each catalog preset, by preset id. Copied from
|
|
29
|
+
* `@skaile/workspaces/bridge`'s `AI_PROVIDER_PRESETS` (9.1); change it there
|
|
30
|
+
* first. A preset this table does not name is `transfer`.
|
|
31
|
+
*/
|
|
32
|
+
export const PRESET_DATA_ZONES: Readonly<Record<string, DataZone>> = {
|
|
33
|
+
anthropic: "transfer",
|
|
34
|
+
openai: "transfer",
|
|
35
|
+
google: "transfer",
|
|
36
|
+
groq: "transfer",
|
|
37
|
+
deepseek: "transfer",
|
|
38
|
+
openrouter: "transfer",
|
|
39
|
+
mistral: "eu",
|
|
40
|
+
xai: "transfer",
|
|
41
|
+
together: "transfer",
|
|
42
|
+
fireworks: "transfer",
|
|
43
|
+
bedrock: "eu_option",
|
|
44
|
+
vertex: "eu_option",
|
|
45
|
+
"azure-openai": "eu_option",
|
|
46
|
+
foundry: "eu_option",
|
|
47
|
+
ollama: "local",
|
|
48
|
+
lmstudio: "local",
|
|
49
|
+
vllm: "local",
|
|
50
|
+
litellm: "transfer",
|
|
51
|
+
deepgram: "transfer",
|
|
52
|
+
elevenlabs: "transfer",
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/** What a provider row's zone chip says. */
|
|
56
|
+
export const DATA_ZONE_LABELS: Readonly<Record<DataZone, string>> = {
|
|
57
|
+
eu: "EU",
|
|
58
|
+
eu_option: "EU option",
|
|
59
|
+
transfer: "Transfer",
|
|
60
|
+
local: "Local",
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/** What a host calls each rule when it names the rule that keeps an entry out. */
|
|
64
|
+
export const DATA_ZONE_RULE_LABELS: Readonly<Record<DataZoneRule, string>> = {
|
|
65
|
+
any: "Anywhere",
|
|
66
|
+
eu_options: "EU, or EU option with EU processing on",
|
|
67
|
+
eu: "EU only",
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/** Whether `value` is one of {@link DATA_ZONES}. */
|
|
71
|
+
export function isDataZone(value: unknown): value is DataZone {
|
|
72
|
+
return typeof value === "string" && (DATA_ZONES as readonly string[]).includes(value);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Whether `value` is one of {@link DATA_ZONE_RULES}. */
|
|
76
|
+
export function isDataZoneRule(value: unknown): value is DataZoneRule {
|
|
77
|
+
return typeof value === "string" && (DATA_ZONE_RULES as readonly string[]).includes(value);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The zone of catalog preset `presetId`, or `undefined` for an id the catalog does not name. */
|
|
81
|
+
export function presetDataZone(presetId: string | undefined): DataZone | undefined {
|
|
82
|
+
return presetId !== undefined && Object.hasOwn(PRESET_DATA_ZONES, presetId)
|
|
83
|
+
? PRESET_DATA_ZONES[presetId]
|
|
84
|
+
: undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Where an entry processes data: its own `data_zone`, else the zone of the
|
|
89
|
+
* catalog preset it names, else `transfer` (an unknown endpoint is assumed to
|
|
90
|
+
* leave the EU).
|
|
91
|
+
*/
|
|
92
|
+
export function entryDataZone(entry: { data_zone?: DataZone; preset?: string }): DataZone {
|
|
93
|
+
return entry.data_zone ?? presetDataZone(entry.preset) ?? "transfer";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Whether `rule` allows an entry in `zone`: `any` allows all; `eu` allows
|
|
98
|
+
* `eu` and `local`; `eu_options` additionally allows `eu_option` when
|
|
99
|
+
* `euProcessing` is `true`.
|
|
100
|
+
*/
|
|
101
|
+
export function dataZoneAllows(
|
|
102
|
+
rule: DataZoneRule,
|
|
103
|
+
zone: DataZone,
|
|
104
|
+
euProcessing: boolean | undefined,
|
|
105
|
+
): boolean {
|
|
106
|
+
if (rule === "any") return true;
|
|
107
|
+
if (zone === "eu" || zone === "local") return true;
|
|
108
|
+
return rule === "eu_options" && zone === "eu_option" && euProcessing === true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Whether `rule` allows `entry` ({@link dataZoneAllows} over {@link entryDataZone}). */
|
|
112
|
+
export function entryAllowedByDataZone(
|
|
113
|
+
entry: { data_zone?: DataZone; preset?: string; eu_processing?: boolean },
|
|
114
|
+
rule: DataZoneRule,
|
|
115
|
+
): boolean {
|
|
116
|
+
return dataZoneAllows(rule, entryDataZone(entry), entry.eu_processing);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Why `rule` keeps an entry in `zone` out, naming the rule, or `undefined`
|
|
121
|
+
* when the rule allows it. Secret-free, safe to render.
|
|
122
|
+
*/
|
|
123
|
+
export function dataZoneRefusal(
|
|
124
|
+
rule: DataZoneRule,
|
|
125
|
+
zone: DataZone,
|
|
126
|
+
euProcessing: boolean | undefined,
|
|
127
|
+
): string | undefined {
|
|
128
|
+
if (dataZoneAllows(rule, zone, euProcessing)) return undefined;
|
|
129
|
+
const why =
|
|
130
|
+
zone === "eu_option" && rule === "eu_options"
|
|
131
|
+
? "EU processing is not on for this account"
|
|
132
|
+
: `this provider is ${DATA_ZONE_LABELS[zone]}`;
|
|
133
|
+
return `Blocked by the data-zone rule "${DATA_ZONE_RULE_LABELS[rule]}": ${why}.`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* The registry with only the entries `rule` allows, every kind alike
|
|
138
|
+
* (`llm`, `embed`, `stt`, `tts`). Order is kept; the input is not mutated. A
|
|
139
|
+
* merged registry comes back merged, its `order`, `origin` and `locked`
|
|
140
|
+
* narrowed to the kept entries.
|
|
141
|
+
*/
|
|
142
|
+
export function filterRegistryByDataZone(
|
|
143
|
+
registry: AIProviderRegistry,
|
|
144
|
+
rule: DataZoneRule,
|
|
145
|
+
): AIProviderRegistry;
|
|
146
|
+
export function filterRegistryByDataZone(
|
|
147
|
+
registry: MergedAIProviderRegistry,
|
|
148
|
+
rule: DataZoneRule,
|
|
149
|
+
): MergedAIProviderRegistry;
|
|
150
|
+
export function filterRegistryByDataZone(
|
|
151
|
+
registry: AIProviderRegistryView,
|
|
152
|
+
rule: DataZoneRule,
|
|
153
|
+
): AIProviderRegistryView;
|
|
154
|
+
export function filterRegistryByDataZone(
|
|
155
|
+
registry: AIProviderRegistryView,
|
|
156
|
+
rule: DataZoneRule,
|
|
157
|
+
): AIProviderRegistryView {
|
|
158
|
+
const entries = isMergedRegistry(registry) ? registry.providers : registry;
|
|
159
|
+
const providers: AIProviderRegistry = {};
|
|
160
|
+
for (const [id, entry] of Object.entries(entries)) {
|
|
161
|
+
if (entryAllowedByDataZone(entry, rule)) providers[id] = entry;
|
|
162
|
+
}
|
|
163
|
+
if (!isMergedRegistry(registry)) return providers;
|
|
164
|
+
const kept = (id: string) => Object.hasOwn(providers, id);
|
|
165
|
+
return {
|
|
166
|
+
...registry,
|
|
167
|
+
providers,
|
|
168
|
+
order: registry.order.filter(kept),
|
|
169
|
+
origin: Object.fromEntries(Object.entries(registry.origin).filter(([id]) => kept(id))),
|
|
170
|
+
locked: registry.locked.filter(kept),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
@@ -8,10 +8,15 @@
|
|
|
8
8
|
* Eligibility is by wire surface, never by vendor name: an entry is eligible
|
|
9
9
|
* for a harness when it is an enabled `llm` entry whose `surface` the harness
|
|
10
10
|
* declares. A harness that declares no surfaces accepts every `llm` entry.
|
|
11
|
+
* A host's data-zone rule then keeps out the entries outside it: such an
|
|
12
|
+
* entry is reported as `blocked-by-rule`, never eligible and never chosen.
|
|
11
13
|
*
|
|
12
14
|
* Browser-safe on purpose — no `node:` imports.
|
|
13
15
|
*/
|
|
14
16
|
|
|
17
|
+
import { DATA_ZONE_RULE_LABELS, dataZoneRefusal, presetDataZone } from "./data-zone";
|
|
18
|
+
import type { DataZone, DataZoneRule } from "./provider-registry";
|
|
19
|
+
|
|
15
20
|
/** How many registry entries a harness can run at once (`@skaile/workspaces` `HarnessInfo.providerPlurality`). */
|
|
16
21
|
export type HarnessProviderPlurality = "registry" | "credentials-n" | "single";
|
|
17
22
|
|
|
@@ -33,6 +38,35 @@ export interface HarnessProviderCandidate {
|
|
|
33
38
|
surface?: string;
|
|
34
39
|
/** Absent means enabled. */
|
|
35
40
|
enabled?: boolean;
|
|
41
|
+
/** Catalog preset; gives the zone when {@link dataZone} is absent. */
|
|
42
|
+
preset?: string;
|
|
43
|
+
/** Effective data zone (`GET /api/providers` rows carry it). Absent: the preset's, else `transfer`. */
|
|
44
|
+
dataZone?: DataZone;
|
|
45
|
+
/** The account's EU-processing setting is on. */
|
|
46
|
+
euProcessing?: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Why an entry on a surface the harness speaks still cannot be used. */
|
|
50
|
+
export type HarnessProviderBlockReason = "blocked-by-rule";
|
|
51
|
+
|
|
52
|
+
/** One entry the harness could speak to that the host's rule keeps out. */
|
|
53
|
+
export interface HarnessProviderBlock {
|
|
54
|
+
id: string;
|
|
55
|
+
reason: HarnessProviderBlockReason;
|
|
56
|
+
/** The rule in force. */
|
|
57
|
+
rule: DataZoneRule;
|
|
58
|
+
/** The rule's label ("EU only"). */
|
|
59
|
+
ruleLabel: string;
|
|
60
|
+
/** The entry's effective zone. */
|
|
61
|
+
zone: DataZone;
|
|
62
|
+
/** What a settings table shows on the box it cannot tick. Secret-free. */
|
|
63
|
+
message: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** What {@link resolveHarnessProviders} applies beyond the surface rule. */
|
|
67
|
+
export interface HarnessProviderOptions {
|
|
68
|
+
/** The host's data-zone rule. Absent: `any`. */
|
|
69
|
+
dataZoneRule?: DataZoneRule;
|
|
36
70
|
}
|
|
37
71
|
|
|
38
72
|
/**
|
|
@@ -55,8 +89,10 @@ export interface HarnessProviderResolution<
|
|
|
55
89
|
state: HarnessProviderState;
|
|
56
90
|
/** The harness can run several providers at once. */
|
|
57
91
|
multiple: boolean;
|
|
58
|
-
/** Stored pick ids that are no longer eligible, deduplicated, in stored order. */
|
|
92
|
+
/** Stored pick ids that are no longer eligible (blocked ones included), deduplicated, in stored order. */
|
|
59
93
|
stale: string[];
|
|
94
|
+
/** Candidates on a surface the harness speaks that the data-zone rule keeps out, in registry order. */
|
|
95
|
+
blocked: HarnessProviderBlock[];
|
|
60
96
|
}
|
|
61
97
|
|
|
62
98
|
/** Whether `candidate` is an enabled `llm` entry on a surface `harness` speaks. */
|
|
@@ -70,22 +106,55 @@ export function isProviderEligibleForHarness(
|
|
|
70
106
|
return candidate.surface !== undefined && harness.surfaces.includes(candidate.surface);
|
|
71
107
|
}
|
|
72
108
|
|
|
109
|
+
/**
|
|
110
|
+
* Why the data-zone rule keeps `candidate` out of `harness`, or `undefined`
|
|
111
|
+
* when it does not: the candidate must be otherwise eligible
|
|
112
|
+
* ({@link isProviderEligibleForHarness}) and outside `rule`.
|
|
113
|
+
*/
|
|
114
|
+
export function harnessProviderBlock(
|
|
115
|
+
harness: HarnessProviderInfo,
|
|
116
|
+
candidate: HarnessProviderCandidate,
|
|
117
|
+
rule: DataZoneRule | undefined,
|
|
118
|
+
): HarnessProviderBlock | undefined {
|
|
119
|
+
if (!rule || rule === "any" || !isProviderEligibleForHarness(harness, candidate)) {
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
const zone = candidate.dataZone ?? presetDataZone(candidate.preset) ?? "transfer";
|
|
123
|
+
const message = dataZoneRefusal(rule, zone, candidate.euProcessing);
|
|
124
|
+
if (!message) return undefined;
|
|
125
|
+
return {
|
|
126
|
+
id: candidate.id,
|
|
127
|
+
reason: "blocked-by-rule",
|
|
128
|
+
rule,
|
|
129
|
+
ruleLabel: DATA_ZONE_RULE_LABELS[rule],
|
|
130
|
+
zone,
|
|
131
|
+
message,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
73
135
|
/**
|
|
74
136
|
* Resolve the providers a harness runs against.
|
|
75
137
|
*
|
|
76
138
|
* @param candidates Registry entries in registry order.
|
|
77
139
|
* @param selection The stored per-harness pick (`SkaileSettings.harnessProviders[harness.id]`).
|
|
140
|
+
* @param options.dataZoneRule The host's data-zone rule: entries outside it are
|
|
141
|
+
* `blocked`, never eligible, never auto-chosen; a stored pick of one is `stale`.
|
|
78
142
|
*/
|
|
79
143
|
export function resolveHarnessProviders<C extends HarnessProviderCandidate>(
|
|
80
144
|
harness: HarnessProviderInfo,
|
|
81
145
|
candidates: readonly C[],
|
|
82
146
|
selection: readonly string[] | undefined,
|
|
147
|
+
options: HarnessProviderOptions = {},
|
|
83
148
|
): HarnessProviderResolution<C> {
|
|
84
149
|
const multiple =
|
|
85
150
|
harness.providerPlurality === "registry" || harness.providerPlurality === "credentials-n";
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
151
|
+
const blocked: HarnessProviderBlock[] = [];
|
|
152
|
+
const eligible = candidates.filter((candidate) => {
|
|
153
|
+
if (!isProviderEligibleForHarness(harness, candidate)) return false;
|
|
154
|
+
const block = harnessProviderBlock(harness, candidate, options.dataZoneRule);
|
|
155
|
+
if (block) blocked.push(block);
|
|
156
|
+
return !block;
|
|
157
|
+
});
|
|
89
158
|
const eligibleIds = new Set(eligible.map((candidate) => candidate.id));
|
|
90
159
|
const picked = new Set(selection ?? []);
|
|
91
160
|
const stale = [...picked].filter((id) => !eligibleIds.has(id));
|
|
@@ -100,10 +169,11 @@ export function resolveHarnessProviders<C extends HarnessProviderCandidate>(
|
|
|
100
169
|
state: "chosen",
|
|
101
170
|
multiple,
|
|
102
171
|
stale,
|
|
172
|
+
blocked,
|
|
103
173
|
};
|
|
104
174
|
}
|
|
105
175
|
if (eligible.length === 0) {
|
|
106
|
-
return { eligible, selected: [], state: "none-eligible", multiple, stale };
|
|
176
|
+
return { eligible, selected: [], state: "none-eligible", multiple, stale, blocked };
|
|
107
177
|
}
|
|
108
178
|
if (eligible.length === 1 || multiple) {
|
|
109
179
|
return {
|
|
@@ -112,7 +182,8 @@ export function resolveHarnessProviders<C extends HarnessProviderCandidate>(
|
|
|
112
182
|
state: "auto",
|
|
113
183
|
multiple,
|
|
114
184
|
stale,
|
|
185
|
+
blocked,
|
|
115
186
|
};
|
|
116
187
|
}
|
|
117
|
-
return { eligible, selected: [], state: "needs-choice", multiple, stale };
|
|
188
|
+
return { eligible, selected: [], state: "needs-choice", multiple, stale, blocked };
|
|
118
189
|
}
|
|
@@ -32,3 +32,28 @@ export const DEFAULT_ENABLED_HARNESSES = ["omp", "claude-sdk", "codex"] as const
|
|
|
32
32
|
export function isExperimentalHarness(id: string): boolean {
|
|
33
33
|
return !(DEFAULT_ENABLED_HARNESSES as readonly string[]).includes(id);
|
|
34
34
|
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The model roles ("models per task") each multi-model harness renders, keyed by
|
|
38
|
+
* harness id. A harness absent here runs one model and takes no roles.
|
|
39
|
+
*/
|
|
40
|
+
export const HARNESS_ROLES: Readonly<Record<string, readonly string[]>> = {
|
|
41
|
+
omp: [
|
|
42
|
+
"default",
|
|
43
|
+
"smol",
|
|
44
|
+
"slow",
|
|
45
|
+
"vision",
|
|
46
|
+
"plan",
|
|
47
|
+
"designer",
|
|
48
|
+
"commit",
|
|
49
|
+
"tiny",
|
|
50
|
+
"task",
|
|
51
|
+
"advisor",
|
|
52
|
+
],
|
|
53
|
+
continue: ["chat", "edit", "apply", "autocomplete", "embed", "rerank", "summarize"],
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/** The role ids `harnessId` supports ({@link HARNESS_ROLES}); empty for a one-model harness. */
|
|
57
|
+
export function harnessRoleIds(harnessId: string): string[] {
|
|
58
|
+
return [...(HARNESS_ROLES[harnessId] ?? [])];
|
|
59
|
+
}
|