@skaile/forge-settings 1.3.3 → 2.0.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 +40 -0
- package/README.md +52 -9
- package/dist/module.json +1 -1
- package/dist/runtime/ai-sdk/index.d.ts +33 -26
- package/dist/runtime/ai-sdk/index.js +34 -16
- 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/editor/index.d.ts +55 -16
- package/dist/runtime/editor/index.js +82 -40
- package/dist/runtime/model/selector.d.ts +16 -0
- package/dist/runtime/model/selector.js +12 -0
- package/dist/runtime/server/credential-store.d.ts +16 -4
- package/dist/runtime/server/credential-store.js +3 -4
- package/dist/runtime/server/env-layers.d.ts +1 -1
- package/dist/runtime/server/env-layers.js +3 -3
- package/dist/runtime/server/index.d.ts +9 -8
- package/dist/runtime/server/index.js +3 -3
- package/dist/runtime/server/provider-discovery.d.ts +28 -8
- package/dist/runtime/server/provider-discovery.js +57 -33
- package/dist/runtime/server/provider-layers.d.ts +1 -1
- package/dist/runtime/server/provider-layers.js +3 -5
- 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 +26 -10
- package/dist/runtime/server/provider-write.js +75 -7
- package/dist/runtime/server/voice-provider.d.ts +61 -0
- package/dist/runtime/server/voice-provider.js +31 -0
- package/dist/runtime/shared/data-zone.d.ts +68 -0
- package/dist/runtime/shared/data-zone.js +79 -0
- package/dist/runtime/shared/harness-providers.d.ts +42 -3
- 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 +19 -182
- package/dist/runtime/shared/provider-registry.js +25 -65
- 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/dist/runtime/{server → workspaces}/harness-settings.d.ts +51 -13
- package/dist/runtime/{server → workspaces}/harness-settings.js +93 -9
- package/dist/runtime/workspaces/index.d.ts +16 -0
- package/dist/runtime/workspaces/index.js +5 -0
- package/dist/runtime/workspaces/language-model.d.ts +45 -0
- package/dist/runtime/workspaces/language-model.js +26 -0
- package/dist/runtime/workspaces/model-endpoint.d.ts +38 -0
- package/dist/runtime/workspaces/model-endpoint.js +54 -0
- package/dist/runtime/workspaces/provider-settings.d.ts +35 -0
- package/dist/runtime/{server → workspaces}/provider-settings.js +0 -31
- package/dist/runtime/{server → workspaces}/settings-defaults.d.ts +1 -1
- package/dist/runtime/{shared → workspaces}/settings-types.d.ts +11 -4
- package/package.json +11 -2
- package/src/runtime/ai-sdk/index.ts +88 -47
- package/src/runtime/components/ForgeProvidersPanel.vue +477 -78
- package/src/runtime/editor/index.ts +158 -63
- package/src/runtime/model/selector.ts +31 -0
- package/src/runtime/server/credential-store.ts +20 -10
- package/src/runtime/server/env-layers.ts +4 -4
- package/src/runtime/server/index.ts +9 -8
- package/src/runtime/server/provider-discovery.ts +114 -48
- package/src/runtime/server/provider-layers.ts +6 -6
- package/src/runtime/server/provider-service.ts +53 -1
- package/src/runtime/server/provider-write.ts +132 -18
- package/src/runtime/server/voice-provider.ts +110 -0
- package/src/runtime/shared/data-zone.ts +179 -0
- package/src/runtime/shared/harness-providers.ts +78 -7
- package/src/runtime/shared/harnesses.ts +25 -0
- package/src/runtime/shared/provider-accounts.ts +526 -0
- package/src/runtime/shared/provider-registry.ts +82 -256
- package/src/runtime/vue/index.ts +2 -0
- package/src/runtime/vue/providers-panel-model.ts +123 -5
- package/src/runtime/vue/useProvidersPanel.ts +589 -37
- package/src/runtime/{server → workspaces}/harness-settings.ts +166 -22
- package/src/runtime/workspaces/index.ts +17 -0
- package/src/runtime/workspaces/language-model.ts +82 -0
- package/src/runtime/workspaces/model-endpoint.ts +100 -0
- package/src/runtime/workspaces/provider-settings.ts +96 -0
- package/src/runtime/{server → workspaces}/settings-defaults.ts +1 -1
- package/src/runtime/{shared → workspaces}/settings-types.ts +11 -4
- package/dist/runtime/server/provider-settings.d.ts +0 -78
- package/src/runtime/server/provider-settings.ts +0 -192
- /package/dist/runtime/{server → workspaces}/settings-defaults.js +0 -0
- /package/dist/runtime/{shared → workspaces}/settings-types.js +0 -0
|
@@ -8,10 +8,13 @@
|
|
|
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
|
+
import type { AIProviderDataZone, AIProviderDataZoneRule } from "./provider-registry.js";
|
|
17
|
+
/** How many registry entries a harness can run at once (`HarnessInfo.providerPlurality` of a Workspaces harness). */
|
|
15
18
|
export type HarnessProviderPlurality = "registry" | "credentials-n" | "single";
|
|
16
19
|
/** The slice of a harness the rule reads. `GET /api/agents` rows satisfy it. */
|
|
17
20
|
export interface HarnessProviderInfo {
|
|
@@ -30,6 +33,32 @@ export interface HarnessProviderCandidate {
|
|
|
30
33
|
surface?: string;
|
|
31
34
|
/** Absent means enabled. */
|
|
32
35
|
enabled?: boolean;
|
|
36
|
+
/** Catalog preset; gives the zone when {@link dataZone} is absent. */
|
|
37
|
+
preset?: string;
|
|
38
|
+
/** Effective data zone (`GET /api/providers` rows carry it). Absent: the preset's, else `transfer`. */
|
|
39
|
+
dataZone?: AIProviderDataZone;
|
|
40
|
+
/** The account's EU-processing setting is on. */
|
|
41
|
+
euProcessing?: boolean;
|
|
42
|
+
}
|
|
43
|
+
/** Why an entry on a surface the harness speaks still cannot be used. */
|
|
44
|
+
export type HarnessProviderBlockReason = "blocked-by-rule";
|
|
45
|
+
/** One entry the harness could speak to that the host's rule keeps out. */
|
|
46
|
+
export interface HarnessProviderBlock {
|
|
47
|
+
id: string;
|
|
48
|
+
reason: HarnessProviderBlockReason;
|
|
49
|
+
/** The rule in force. */
|
|
50
|
+
rule: AIProviderDataZoneRule;
|
|
51
|
+
/** The rule's label ("EU only"). */
|
|
52
|
+
ruleLabel: string;
|
|
53
|
+
/** The entry's effective zone. */
|
|
54
|
+
zone: AIProviderDataZone;
|
|
55
|
+
/** What a settings table shows on the box it cannot tick. Secret-free. */
|
|
56
|
+
message: string;
|
|
57
|
+
}
|
|
58
|
+
/** What {@link resolveHarnessProviders} applies beyond the surface rule. */
|
|
59
|
+
export interface HarnessProviderOptions {
|
|
60
|
+
/** The host's data-zone rule. Absent: `any`. */
|
|
61
|
+
dataZoneRule?: AIProviderDataZoneRule;
|
|
33
62
|
}
|
|
34
63
|
/**
|
|
35
64
|
* - `chosen`: the stored pick, narrowed to what is still eligible, is used.
|
|
@@ -48,15 +77,25 @@ export interface HarnessProviderResolution<C extends HarnessProviderCandidate =
|
|
|
48
77
|
state: HarnessProviderState;
|
|
49
78
|
/** The harness can run several providers at once. */
|
|
50
79
|
multiple: boolean;
|
|
51
|
-
/** Stored pick ids that are no longer eligible, deduplicated, in stored order. */
|
|
80
|
+
/** Stored pick ids that are no longer eligible (blocked ones included), deduplicated, in stored order. */
|
|
52
81
|
stale: string[];
|
|
82
|
+
/** Candidates on a surface the harness speaks that the data-zone rule keeps out, in registry order. */
|
|
83
|
+
blocked: HarnessProviderBlock[];
|
|
53
84
|
}
|
|
54
85
|
/** Whether `candidate` is an enabled `llm` entry on a surface `harness` speaks. */
|
|
55
86
|
export declare function isProviderEligibleForHarness(harness: HarnessProviderInfo, candidate: HarnessProviderCandidate): boolean;
|
|
87
|
+
/**
|
|
88
|
+
* Why the data-zone rule keeps `candidate` out of `harness`, or `undefined`
|
|
89
|
+
* when it does not: the candidate must be otherwise eligible
|
|
90
|
+
* ({@link isProviderEligibleForHarness}) and outside `rule`.
|
|
91
|
+
*/
|
|
92
|
+
export declare function harnessProviderBlock(harness: HarnessProviderInfo, candidate: HarnessProviderCandidate, rule: AIProviderDataZoneRule | undefined): HarnessProviderBlock | undefined;
|
|
56
93
|
/**
|
|
57
94
|
* Resolve the providers a harness runs against.
|
|
58
95
|
*
|
|
59
96
|
* @param candidates Registry entries in registry order.
|
|
60
97
|
* @param selection The stored per-harness pick (`SkaileSettings.harnessProviders[harness.id]`).
|
|
98
|
+
* @param options.dataZoneRule The host's data-zone rule: entries outside it are
|
|
99
|
+
* `blocked`, never eligible, never auto-chosen; a stored pick of one is `stale`.
|
|
61
100
|
*/
|
|
62
|
-
export declare function resolveHarnessProviders<C extends HarnessProviderCandidate>(harness: HarnessProviderInfo, candidates: readonly C[], selection: readonly string[] | undefined): HarnessProviderResolution<C>;
|
|
101
|
+
export declare function resolveHarnessProviders<C extends HarnessProviderCandidate>(harness: HarnessProviderInfo, candidates: readonly C[], selection: readonly string[] | undefined, options?: HarnessProviderOptions): HarnessProviderResolution<C>;
|
|
@@ -1,14 +1,35 @@
|
|
|
1
|
+
import { DATA_ZONE_RULE_LABELS, dataZoneRefusal, presetDataZone } from "./data-zone.js";
|
|
1
2
|
export function isProviderEligibleForHarness(harness, candidate) {
|
|
2
3
|
if ((candidate.kind ?? "llm") !== "llm") return false;
|
|
3
4
|
if (candidate.enabled === false) return false;
|
|
4
5
|
if (!harness.surfaces) return true;
|
|
5
6
|
return candidate.surface !== void 0 && harness.surfaces.includes(candidate.surface);
|
|
6
7
|
}
|
|
7
|
-
export function
|
|
8
|
+
export function harnessProviderBlock(harness, candidate, rule) {
|
|
9
|
+
if (!rule || rule === "any" || !isProviderEligibleForHarness(harness, candidate)) {
|
|
10
|
+
return void 0;
|
|
11
|
+
}
|
|
12
|
+
const zone = candidate.dataZone ?? presetDataZone(candidate.preset) ?? "transfer";
|
|
13
|
+
const message = dataZoneRefusal(rule, zone, candidate.euProcessing);
|
|
14
|
+
if (!message) return void 0;
|
|
15
|
+
return {
|
|
16
|
+
id: candidate.id,
|
|
17
|
+
reason: "blocked-by-rule",
|
|
18
|
+
rule,
|
|
19
|
+
ruleLabel: DATA_ZONE_RULE_LABELS[rule],
|
|
20
|
+
zone,
|
|
21
|
+
message
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export function resolveHarnessProviders(harness, candidates, selection, options = {}) {
|
|
8
25
|
const multiple = harness.providerPlurality === "registry" || harness.providerPlurality === "credentials-n";
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
26
|
+
const blocked = [];
|
|
27
|
+
const eligible = candidates.filter((candidate) => {
|
|
28
|
+
if (!isProviderEligibleForHarness(harness, candidate)) return false;
|
|
29
|
+
const block = harnessProviderBlock(harness, candidate, options.dataZoneRule);
|
|
30
|
+
if (block) blocked.push(block);
|
|
31
|
+
return !block;
|
|
32
|
+
});
|
|
12
33
|
const eligibleIds = new Set(eligible.map((candidate) => candidate.id));
|
|
13
34
|
const picked = new Set(selection ?? []);
|
|
14
35
|
const stale = [...picked].filter((id) => !eligibleIds.has(id));
|
|
@@ -19,11 +40,12 @@ export function resolveHarnessProviders(harness, candidates, selection) {
|
|
|
19
40
|
selected: multiple ? usable : usable.slice(0, 1),
|
|
20
41
|
state: "chosen",
|
|
21
42
|
multiple,
|
|
22
|
-
stale
|
|
43
|
+
stale,
|
|
44
|
+
blocked
|
|
23
45
|
};
|
|
24
46
|
}
|
|
25
47
|
if (eligible.length === 0) {
|
|
26
|
-
return { eligible, selected: [], state: "none-eligible", multiple, stale };
|
|
48
|
+
return { eligible, selected: [], state: "none-eligible", multiple, stale, blocked };
|
|
27
49
|
}
|
|
28
50
|
if (eligible.length === 1 || multiple) {
|
|
29
51
|
return {
|
|
@@ -31,8 +53,9 @@ export function resolveHarnessProviders(harness, candidates, selection) {
|
|
|
31
53
|
selected: eligible.map((candidate) => candidate.id),
|
|
32
54
|
state: "auto",
|
|
33
55
|
multiple,
|
|
34
|
-
stale
|
|
56
|
+
stale,
|
|
57
|
+
blocked
|
|
35
58
|
};
|
|
36
59
|
}
|
|
37
|
-
return { eligible, selected: [], state: "needs-choice", multiple, stale };
|
|
60
|
+
return { eligible, selected: [], state: "needs-choice", multiple, stale, blocked };
|
|
38
61
|
}
|
|
@@ -17,3 +17,10 @@ export declare const HARNESS_LABELS: Record<string, string>;
|
|
|
17
17
|
export declare const DEFAULT_ENABLED_HARNESSES: readonly ["omp", "claude-sdk", "codex"];
|
|
18
18
|
/** Whether `id` is outside {@link DEFAULT_ENABLED_HARNESSES}, i.e. opt-in only. */
|
|
19
19
|
export declare function isExperimentalHarness(id: string): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* The model roles ("models per task") each multi-model harness renders, keyed by
|
|
22
|
+
* harness id. A harness absent here runs one model and takes no roles.
|
|
23
|
+
*/
|
|
24
|
+
export declare const HARNESS_ROLES: Readonly<Record<string, readonly string[]>>;
|
|
25
|
+
/** The role ids `harnessId` supports ({@link HARNESS_ROLES}); empty for a one-model harness. */
|
|
26
|
+
export declare function harnessRoleIds(harnessId: string): string[];
|
|
@@ -13,3 +13,21 @@ export const DEFAULT_ENABLED_HARNESSES = ["omp", "claude-sdk", "codex"];
|
|
|
13
13
|
export function isExperimentalHarness(id) {
|
|
14
14
|
return !DEFAULT_ENABLED_HARNESSES.includes(id);
|
|
15
15
|
}
|
|
16
|
+
export const HARNESS_ROLES = {
|
|
17
|
+
omp: [
|
|
18
|
+
"default",
|
|
19
|
+
"smol",
|
|
20
|
+
"slow",
|
|
21
|
+
"vision",
|
|
22
|
+
"plan",
|
|
23
|
+
"designer",
|
|
24
|
+
"commit",
|
|
25
|
+
"tiny",
|
|
26
|
+
"task",
|
|
27
|
+
"advisor"
|
|
28
|
+
],
|
|
29
|
+
continue: ["chat", "edit", "apply", "autocomplete", "embed", "rerank", "summarize"]
|
|
30
|
+
};
|
|
31
|
+
export function harnessRoleIds(harnessId) {
|
|
32
|
+
return [...HARNESS_ROLES[harnessId] ?? []];
|
|
33
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider accounts: how one signs in to a catalog preset, what a provider
|
|
3
|
+
* row says about its account, and the Microsoft Azure account that writes up
|
|
4
|
+
* to two registry entries (Azure OpenAI and Claude on Foundry) sharing one
|
|
5
|
+
* resource and one credential.
|
|
6
|
+
*
|
|
7
|
+
* Pure and browser-safe: the settings panel and the server's write gate read
|
|
8
|
+
* the same rules. The facts come from the catalog preset
|
|
9
|
+
* (`@skaile/provider-registry/catalog`'s `AIProviderPreset`): its `credential.kind`, its
|
|
10
|
+
* `transport`, its default `params` and its `required_params`. The catalog
|
|
11
|
+
* names no list of accepted sign-in kinds, so they are derived here from those
|
|
12
|
+
* facts.
|
|
13
|
+
*/
|
|
14
|
+
import { type AIProviderCredential, type AIProviderCredentialKind, type AIProviderTransport } from "./provider-registry.js";
|
|
15
|
+
/** The preset facts the account helpers read; a catalog preset and its summary both fit. */
|
|
16
|
+
export interface ProviderAccountPreset {
|
|
17
|
+
id: string;
|
|
18
|
+
label: string;
|
|
19
|
+
transport?: AIProviderTransport;
|
|
20
|
+
credential: {
|
|
21
|
+
kind: AIProviderCredentialKind;
|
|
22
|
+
env?: string;
|
|
23
|
+
};
|
|
24
|
+
/** Default routing parameters. */
|
|
25
|
+
params?: Record<string, string>;
|
|
26
|
+
/** `params` keys the operator must supply. */
|
|
27
|
+
required_params?: string[];
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The `params` keys a preset requires that `params` leaves empty: the
|
|
31
|
+
* preset's `required_params` minus the ones its own `params` default.
|
|
32
|
+
* A value that is not a non-blank string, number or boolean counts as missing.
|
|
33
|
+
*/
|
|
34
|
+
export declare function missingRequiredParams(preset: Pick<ProviderAccountPreset, "required_params" | "params">, params: Record<string, unknown> | undefined): string[];
|
|
35
|
+
/** What the add/edit dialog calls each routing parameter. */
|
|
36
|
+
export declare const PROVIDER_PARAM_LABELS: Record<string, string>;
|
|
37
|
+
/**
|
|
38
|
+
* Parameters the operator sets only in expert mode: the dialog prefills them
|
|
39
|
+
* and shows them read-only otherwise.
|
|
40
|
+
*/
|
|
41
|
+
export declare const EXPERT_PARAMS: readonly string[];
|
|
42
|
+
/**
|
|
43
|
+
* The parameter fields an entry of `preset` needs: its required params the
|
|
44
|
+
* preset gives no default for, then any further key `current` already holds.
|
|
45
|
+
*/
|
|
46
|
+
export declare function presetParamKeys(preset: Pick<ProviderAccountPreset, "required_params" | "params"> | null | undefined, current?: Record<string, unknown> | undefined): string[];
|
|
47
|
+
/** The value the dialog prefills for parameter `key` of `presetId`, else `""`. */
|
|
48
|
+
export declare function defaultParamValue(presetId: string | undefined, key: string): string;
|
|
49
|
+
/** One way to sign in to a provider, as the add flow's "How do you sign in?" step lists it. */
|
|
50
|
+
export type ProviderSignInId = "api_key" | "entra" | "aws" | "google" | "oauth" | "none" | "reference";
|
|
51
|
+
/** What the sign-in step offers for one {@link ProviderSignInId}. */
|
|
52
|
+
export interface ProviderSignInOption {
|
|
53
|
+
id: ProviderSignInId;
|
|
54
|
+
label: string;
|
|
55
|
+
description: string;
|
|
56
|
+
/** The `credential.kind` the entry gets. */
|
|
57
|
+
credentialKind: AIProviderCredentialKind;
|
|
58
|
+
/**
|
|
59
|
+
* The fields the next step shows: `key` = environment variable name plus
|
|
60
|
+
* an optional value for the scope's `.env`; `reference` = a typed
|
|
61
|
+
* `env:` / `keyring:` / `cmd:` / `platform:` reference; `none` = nothing
|
|
62
|
+
* (no credential, or the server's ambient cloud chain signs).
|
|
63
|
+
*/
|
|
64
|
+
fields: "key" | "reference" | "none";
|
|
65
|
+
}
|
|
66
|
+
/** Transports that run on a Microsoft Azure resource. */
|
|
67
|
+
export declare const AZURE_TRANSPORTS: readonly AIProviderTransport[];
|
|
68
|
+
/** Whether a transport runs on a Microsoft Azure resource (and so accepts Entra ID). */
|
|
69
|
+
export declare function isAzureTransport(transport: AIProviderTransport | undefined): boolean;
|
|
70
|
+
/**
|
|
71
|
+
* The sign-in kinds a preset accepts, in the order the sign-in step lists
|
|
72
|
+
* them. `null` is a custom entry (no preset).
|
|
73
|
+
*
|
|
74
|
+
* - `none` presets (local model servers): No sign-in only.
|
|
75
|
+
* - `api_key_header` / `bearer` presets: API key; Azure transports
|
|
76
|
+
* (`azure-openai`, `foundry`) list Entra ID first, then API key.
|
|
77
|
+
* - `oauth` / `command` presets: a reference (the token lives behind one).
|
|
78
|
+
* - `expert`: the raw Reference form is added for every preset that takes a
|
|
79
|
+
* credential.
|
|
80
|
+
*/
|
|
81
|
+
export declare function presetSignInOptions(preset: Pick<ProviderAccountPreset, "credential" | "transport"> | null, options?: {
|
|
82
|
+
expert?: boolean;
|
|
83
|
+
}): ProviderSignInOption[];
|
|
84
|
+
/** What the sign-in fields hold. */
|
|
85
|
+
export interface ProviderSignInInput {
|
|
86
|
+
/** `key` fields: the environment variable the key lives in. */
|
|
87
|
+
envName?: string;
|
|
88
|
+
/** `reference` fields: the typed reference. */
|
|
89
|
+
ref?: string;
|
|
90
|
+
}
|
|
91
|
+
/** The credential a sign-in choice writes, or why it cannot be written. */
|
|
92
|
+
export type ProviderSignInCredential = {
|
|
93
|
+
ok: true;
|
|
94
|
+
credential?: AIProviderCredential;
|
|
95
|
+
} | {
|
|
96
|
+
ok: false;
|
|
97
|
+
error: string;
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* The `credential` of an entry signed in with `option`.
|
|
101
|
+
*
|
|
102
|
+
* `key`: `{ kind, ref: "env:<NAME>" }`; a blank name falls back to
|
|
103
|
+
* `defaultEnv` (the preset's conventional variable) and, without one, writes
|
|
104
|
+
* no credential so the preset's own applies — unless `requireCredential`.
|
|
105
|
+
* `reference`: `{ kind, ref }` after the reference check. `none` fields: a
|
|
106
|
+
* cloud chain writes `{ kind }` without a reference, No sign-in writes
|
|
107
|
+
* `{ kind: "none" }`.
|
|
108
|
+
*/
|
|
109
|
+
export declare function signInCredential(option: ProviderSignInOption, input: ProviderSignInInput, context?: {
|
|
110
|
+
defaultEnv?: string;
|
|
111
|
+
requireCredential?: boolean;
|
|
112
|
+
}): ProviderSignInCredential;
|
|
113
|
+
/** The sign-in option an existing credential corresponds to, among `options`. */
|
|
114
|
+
export declare function signInOptionOf(credential: {
|
|
115
|
+
kind?: AIProviderCredentialKind;
|
|
116
|
+
ref?: string;
|
|
117
|
+
} | undefined, options: readonly ProviderSignInOption[]): ProviderSignInOption | undefined;
|
|
118
|
+
/** What a row says for each credential kind. */
|
|
119
|
+
export declare const CREDENTIAL_KIND_LABELS: Record<AIProviderCredentialKind, string>;
|
|
120
|
+
/** The family an Azure-transport entry belongs to. */
|
|
121
|
+
export declare const AZURE_FAMILY_LABEL = "Microsoft Azure";
|
|
122
|
+
/**
|
|
123
|
+
* The provider family a row belongs to: "Microsoft Azure" for an
|
|
124
|
+
* Azure transport, else its preset's label, else "Custom".
|
|
125
|
+
*/
|
|
126
|
+
export declare function providerFamilyLabel(entry: {
|
|
127
|
+
preset?: string;
|
|
128
|
+
transport?: AIProviderTransport;
|
|
129
|
+
}, presets: readonly Pick<ProviderAccountPreset, "id" | "label" | "transport">[]): string;
|
|
130
|
+
/**
|
|
131
|
+
* The account line of a provider row: family · sign-in kind · resource, each
|
|
132
|
+
* part only when known — "Microsoft Azure · Entra ID · contoso-ai".
|
|
133
|
+
*/
|
|
134
|
+
export declare function providerAccountLine(entry: {
|
|
135
|
+
preset?: string;
|
|
136
|
+
transport?: AIProviderTransport;
|
|
137
|
+
credentialKind?: AIProviderCredentialKind;
|
|
138
|
+
transportParams?: Record<string, string>;
|
|
139
|
+
}, presets: readonly Pick<ProviderAccountPreset, "id" | "label" | "transport" | "credential">[]): string;
|
|
140
|
+
/** A credential's expiry as a row shows it. */
|
|
141
|
+
export interface CredentialExpiry {
|
|
142
|
+
/** Whole days left, rounded down; `0` = expires within a day, negative = expired. */
|
|
143
|
+
days: number;
|
|
144
|
+
expired: boolean;
|
|
145
|
+
/** "Expires in 12 days" · "Expires in 1 day" · "Expires today" · "Expired". */
|
|
146
|
+
text: string;
|
|
147
|
+
}
|
|
148
|
+
/** The expiry line for an ISO timestamp, or `null` when there is none or it does not parse. */
|
|
149
|
+
export declare function credentialExpiry(expiresAt: string | null | undefined, now?: number): CredentialExpiry | null;
|
|
150
|
+
/** Catalog preset of the Azure OpenAI entry. */
|
|
151
|
+
export declare const AZURE_OPENAI_PRESET = "azure-openai";
|
|
152
|
+
/** Catalog preset of the Claude on Foundry entry. */
|
|
153
|
+
export declare const FOUNDRY_PRESET = "foundry";
|
|
154
|
+
/**
|
|
155
|
+
* The `api_version` the Azure account prefills when the preset names none
|
|
156
|
+
* (the catalog's `azure-openai` preset requires it but has no default).
|
|
157
|
+
*/
|
|
158
|
+
export declare const AZURE_OPENAI_DEFAULT_API_VERSION = "2025-04-01-preview";
|
|
159
|
+
/** One deployment row: the Azure deployment name and the logical model id it serves. */
|
|
160
|
+
export interface ProviderDeploymentRow {
|
|
161
|
+
deployment: string;
|
|
162
|
+
/** Logical model id (the `model_map` key). Blank: the deployment name. */
|
|
163
|
+
model: string;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* `model_map` (logical id → deployment name) from deployment rows. Rows with
|
|
167
|
+
* both fields blank are skipped; a model without a deployment, or two rows
|
|
168
|
+
* naming the same logical id, is refused.
|
|
169
|
+
*/
|
|
170
|
+
export declare function deploymentModelMap(rows: readonly ProviderDeploymentRow[]): {
|
|
171
|
+
ok: true;
|
|
172
|
+
modelMap: Record<string, string>;
|
|
173
|
+
} | {
|
|
174
|
+
ok: false;
|
|
175
|
+
error: string;
|
|
176
|
+
};
|
|
177
|
+
/** What the Microsoft Azure card collects. */
|
|
178
|
+
export interface AzureAccountInput {
|
|
179
|
+
/** Azure resource name: the `{resource}` of `<resource>.openai.azure.com` / `<resource>.services.ai.azure.com`. */
|
|
180
|
+
resource: string;
|
|
181
|
+
/** The one credential both entries get (from {@link signInCredential}). */
|
|
182
|
+
credential?: AIProviderCredential;
|
|
183
|
+
/** Set: write an Azure OpenAI entry. At least one deployment. */
|
|
184
|
+
openai?: {
|
|
185
|
+
deployments: readonly ProviderDeploymentRow[];
|
|
186
|
+
apiVersion: string;
|
|
187
|
+
};
|
|
188
|
+
/** Set: write a Claude on Foundry entry; deployments are optional. */
|
|
189
|
+
foundry?: {
|
|
190
|
+
deployments: readonly ProviderDeploymentRow[];
|
|
191
|
+
};
|
|
192
|
+
/** Entry ids the scope already uses; new ids avoid them. */
|
|
193
|
+
takenIds?: readonly string[];
|
|
194
|
+
}
|
|
195
|
+
/** One entry to `PUT`: its id and body. */
|
|
196
|
+
export interface ProviderEntryDraft {
|
|
197
|
+
id: string;
|
|
198
|
+
body: Record<string, unknown>;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* The registry entries one Microsoft Azure account writes: an Azure OpenAI
|
|
202
|
+
* entry (preset `azure-openai`, `params` `{ resource, deployment, api_version }`
|
|
203
|
+
* with the first deployment as the routed one, `model_map` from the
|
|
204
|
+
* deployment rows) and/or a Claude on Foundry entry (preset `foundry`,
|
|
205
|
+
* `params.resource`, optional `model_map`). Both carry the same resource and
|
|
206
|
+
* the same credential. Ids are the preset ids, else `<preset>-<resource>`,
|
|
207
|
+
* else numbered, never one of `takenIds`.
|
|
208
|
+
*/
|
|
209
|
+
export declare function azureAccountEntries(input: AzureAccountInput): {
|
|
210
|
+
ok: true;
|
|
211
|
+
entries: ProviderEntryDraft[];
|
|
212
|
+
} | {
|
|
213
|
+
ok: false;
|
|
214
|
+
error: string;
|
|
215
|
+
};
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import {
|
|
2
|
+
validateCredentialRef
|
|
3
|
+
} from "./provider-registry.js";
|
|
4
|
+
export function missingRequiredParams(preset, params) {
|
|
5
|
+
return (preset.required_params ?? []).filter((key) => {
|
|
6
|
+
if (preset.params?.[key] !== void 0) return false;
|
|
7
|
+
const value = params?.[key];
|
|
8
|
+
if (typeof value === "number" || typeof value === "boolean") return false;
|
|
9
|
+
return typeof value !== "string" || value.trim() === "";
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
export const PROVIDER_PARAM_LABELS = {
|
|
13
|
+
resource: "Resource",
|
|
14
|
+
deployment: "Deployment",
|
|
15
|
+
api_version: "API version",
|
|
16
|
+
region: "Region",
|
|
17
|
+
project: "Project",
|
|
18
|
+
location: "Location",
|
|
19
|
+
publisher: "Publisher"
|
|
20
|
+
};
|
|
21
|
+
export const EXPERT_PARAMS = ["api_version"];
|
|
22
|
+
export function presetParamKeys(preset, current = void 0) {
|
|
23
|
+
const keys = (preset?.required_params ?? []).filter((key) => preset?.params?.[key] === void 0);
|
|
24
|
+
for (const key of Object.keys(current ?? {})) if (!keys.includes(key)) keys.push(key);
|
|
25
|
+
return keys;
|
|
26
|
+
}
|
|
27
|
+
export function defaultParamValue(presetId, key) {
|
|
28
|
+
return presetId === AZURE_OPENAI_PRESET && key === "api_version" ? AZURE_OPENAI_DEFAULT_API_VERSION : "";
|
|
29
|
+
}
|
|
30
|
+
export const AZURE_TRANSPORTS = ["azure-openai", "foundry"];
|
|
31
|
+
export function isAzureTransport(transport) {
|
|
32
|
+
return transport !== void 0 && AZURE_TRANSPORTS.includes(transport);
|
|
33
|
+
}
|
|
34
|
+
function apiKeyOption(kind) {
|
|
35
|
+
return {
|
|
36
|
+
id: "api_key",
|
|
37
|
+
label: "API key",
|
|
38
|
+
description: "A key from the provider. It is stored in this scope's environment; the entry only names the variable.",
|
|
39
|
+
credentialKind: kind,
|
|
40
|
+
fields: "key"
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
const ENTRA_OPTION = {
|
|
44
|
+
id: "entra",
|
|
45
|
+
label: "Entra ID",
|
|
46
|
+
description: "Microsoft Entra ID: the server's Azure identity signs the requests (managed identity, az login or AZURE_CLIENT_ID / AZURE_CLIENT_SECRET / AZURE_TENANT_ID). No key is stored.",
|
|
47
|
+
credentialKind: "azure_entra",
|
|
48
|
+
fields: "none"
|
|
49
|
+
};
|
|
50
|
+
const AWS_OPTION = {
|
|
51
|
+
id: "aws",
|
|
52
|
+
label: "AWS credentials",
|
|
53
|
+
description: "The server's AWS default credential chain signs the requests. No key is stored.",
|
|
54
|
+
credentialKind: "sigv4",
|
|
55
|
+
fields: "none"
|
|
56
|
+
};
|
|
57
|
+
const GOOGLE_OPTION = {
|
|
58
|
+
id: "google",
|
|
59
|
+
label: "Google credentials",
|
|
60
|
+
description: "Google application default credentials on the server sign the requests. No key is stored.",
|
|
61
|
+
credentialKind: "google_adc",
|
|
62
|
+
fields: "none"
|
|
63
|
+
};
|
|
64
|
+
const OAUTH_OPTION = {
|
|
65
|
+
id: "oauth",
|
|
66
|
+
label: "OAuth",
|
|
67
|
+
description: "An OAuth token held behind a reference (keyring, command or control plane).",
|
|
68
|
+
credentialKind: "oauth",
|
|
69
|
+
fields: "reference"
|
|
70
|
+
};
|
|
71
|
+
const NONE_OPTION = {
|
|
72
|
+
id: "none",
|
|
73
|
+
label: "No sign-in",
|
|
74
|
+
description: "The endpoint takes no credential, such as a model server on this machine.",
|
|
75
|
+
credentialKind: "none",
|
|
76
|
+
fields: "none"
|
|
77
|
+
};
|
|
78
|
+
function referenceOption(kind) {
|
|
79
|
+
return {
|
|
80
|
+
id: "reference",
|
|
81
|
+
label: "Reference",
|
|
82
|
+
description: "Type the credential reference yourself: env:NAME, keyring:service/account, cmd:\u2026 or platform:\u2026.",
|
|
83
|
+
credentialKind: kind,
|
|
84
|
+
fields: "reference"
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
export function presetSignInOptions(preset, options = {}) {
|
|
88
|
+
const out = [];
|
|
89
|
+
const kind = preset?.credential.kind;
|
|
90
|
+
if (!preset) {
|
|
91
|
+
out.push(apiKeyOption("bearer"), NONE_OPTION);
|
|
92
|
+
} else if (kind === "none") {
|
|
93
|
+
out.push(NONE_OPTION);
|
|
94
|
+
} else if (kind === "sigv4") {
|
|
95
|
+
out.push(AWS_OPTION);
|
|
96
|
+
} else if (kind === "google_adc") {
|
|
97
|
+
out.push(GOOGLE_OPTION);
|
|
98
|
+
} else if (kind === "azure_entra") {
|
|
99
|
+
out.push(ENTRA_OPTION);
|
|
100
|
+
} else if (kind === "oauth") {
|
|
101
|
+
out.push(OAUTH_OPTION);
|
|
102
|
+
} else if (kind === "command") {
|
|
103
|
+
out.push(referenceOption("command"));
|
|
104
|
+
} else if (kind === "api_key_header" || kind === "bearer") {
|
|
105
|
+
if (isAzureTransport(preset.transport)) out.push(ENTRA_OPTION);
|
|
106
|
+
out.push(apiKeyOption(kind));
|
|
107
|
+
}
|
|
108
|
+
const referenceKind = kind ?? "bearer";
|
|
109
|
+
if (options.expert && referenceKind !== "none" && !out.some((o) => o.fields === "reference")) {
|
|
110
|
+
out.push(referenceOption(referenceKind));
|
|
111
|
+
}
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
115
|
+
export function signInCredential(option, input, context = {}) {
|
|
116
|
+
if (option.fields === "none") return { ok: true, credential: { kind: option.credentialKind } };
|
|
117
|
+
if (option.fields === "reference") {
|
|
118
|
+
const ref = input.ref?.trim() ?? "";
|
|
119
|
+
if (!ref) return { ok: false, error: "Enter a credential reference." };
|
|
120
|
+
const refusal = validateCredentialRef(ref);
|
|
121
|
+
if (refusal) return { ok: false, error: refusal };
|
|
122
|
+
return { ok: true, credential: { kind: option.credentialKind, ref } };
|
|
123
|
+
}
|
|
124
|
+
const name = input.envName?.trim() || context.defaultEnv || "";
|
|
125
|
+
if (!name) {
|
|
126
|
+
return context.requireCredential ? { ok: false, error: "Name the environment variable the key is stored in." } : { ok: true };
|
|
127
|
+
}
|
|
128
|
+
if (!ENV_NAME.test(name)) {
|
|
129
|
+
return { ok: false, error: `'${name}' is not a valid environment variable name.` };
|
|
130
|
+
}
|
|
131
|
+
return { ok: true, credential: { kind: option.credentialKind, ref: `env:${name}` } };
|
|
132
|
+
}
|
|
133
|
+
export function signInOptionOf(credential, options) {
|
|
134
|
+
if (!credential?.kind) return options[0];
|
|
135
|
+
const sameKind = options.filter((o) => o.credentialKind === credential.kind);
|
|
136
|
+
const ref = credential.ref;
|
|
137
|
+
const wanted = !ref ? sameKind.some((o) => o.fields === "none") ? "none" : "key" : ref.startsWith("env:") ? "key" : "reference";
|
|
138
|
+
return sameKind.find((o) => o.fields === wanted) ?? sameKind[0];
|
|
139
|
+
}
|
|
140
|
+
export const CREDENTIAL_KIND_LABELS = {
|
|
141
|
+
api_key_header: "API key",
|
|
142
|
+
bearer: "API key",
|
|
143
|
+
azure_entra: "Entra ID",
|
|
144
|
+
sigv4: "AWS credentials",
|
|
145
|
+
google_adc: "Google credentials",
|
|
146
|
+
oauth: "OAuth",
|
|
147
|
+
command: "Command",
|
|
148
|
+
none: "No sign-in"
|
|
149
|
+
};
|
|
150
|
+
export const AZURE_FAMILY_LABEL = "Microsoft Azure";
|
|
151
|
+
export function providerFamilyLabel(entry, presets) {
|
|
152
|
+
const preset = entry.preset ? presets.find((p) => p.id === entry.preset) : void 0;
|
|
153
|
+
if (isAzureTransport(entry.transport ?? preset?.transport)) return AZURE_FAMILY_LABEL;
|
|
154
|
+
return preset?.label ?? (entry.preset ? entry.preset : "Custom");
|
|
155
|
+
}
|
|
156
|
+
export function providerAccountLine(entry, presets) {
|
|
157
|
+
const preset = entry.preset ? presets.find((p) => p.id === entry.preset) : void 0;
|
|
158
|
+
const kind = entry.credentialKind ?? preset?.credential.kind;
|
|
159
|
+
const resource = entry.transportParams?.resource?.trim();
|
|
160
|
+
return [
|
|
161
|
+
providerFamilyLabel(entry, presets),
|
|
162
|
+
kind ? CREDENTIAL_KIND_LABELS[kind] : null,
|
|
163
|
+
resource || null
|
|
164
|
+
].filter((part) => !!part).join(" \xB7 ");
|
|
165
|
+
}
|
|
166
|
+
const DAY_MS = 24 * 60 * 60 * 1e3;
|
|
167
|
+
export function credentialExpiry(expiresAt, now = Date.now()) {
|
|
168
|
+
if (!expiresAt) return null;
|
|
169
|
+
const at = Date.parse(expiresAt);
|
|
170
|
+
if (!Number.isFinite(at)) return null;
|
|
171
|
+
if (at <= now) return { days: Math.floor((at - now) / DAY_MS), expired: true, text: "Expired" };
|
|
172
|
+
const days = Math.floor((at - now) / DAY_MS);
|
|
173
|
+
const text = days === 0 ? "Expires today" : `Expires in ${days} day${days === 1 ? "" : "s"}`;
|
|
174
|
+
return { days, expired: false, text };
|
|
175
|
+
}
|
|
176
|
+
export const AZURE_OPENAI_PRESET = "azure-openai";
|
|
177
|
+
export const FOUNDRY_PRESET = "foundry";
|
|
178
|
+
export const AZURE_OPENAI_DEFAULT_API_VERSION = "2025-04-01-preview";
|
|
179
|
+
export function deploymentModelMap(rows) {
|
|
180
|
+
const modelMap = {};
|
|
181
|
+
for (const row of rows) {
|
|
182
|
+
const deployment = row.deployment.trim();
|
|
183
|
+
const model = row.model.trim();
|
|
184
|
+
if (!deployment && !model) continue;
|
|
185
|
+
if (!deployment) return { ok: false, error: `Name the deployment that serves '${model}'.` };
|
|
186
|
+
const id = model || deployment;
|
|
187
|
+
if (Object.hasOwn(modelMap, id)) return { ok: false, error: `Model '${id}' is listed twice.` };
|
|
188
|
+
modelMap[id] = deployment;
|
|
189
|
+
}
|
|
190
|
+
return { ok: true, modelMap };
|
|
191
|
+
}
|
|
192
|
+
const AZURE_RESOURCE = /^[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?$/;
|
|
193
|
+
function freeId(base, resource, taken) {
|
|
194
|
+
if (!taken.has(base)) return base;
|
|
195
|
+
const slug = resource.toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
196
|
+
const withResource = slug ? `${base}-${slug}` : base;
|
|
197
|
+
if (!taken.has(withResource)) return withResource;
|
|
198
|
+
for (let n = 2; ; n++) {
|
|
199
|
+
const candidate = `${withResource}-${n}`;
|
|
200
|
+
if (!taken.has(candidate)) return candidate;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
export function azureAccountEntries(input) {
|
|
204
|
+
const resource = input.resource.trim();
|
|
205
|
+
if (!resource) return { ok: false, error: "Enter the Azure resource name." };
|
|
206
|
+
if (!AZURE_RESOURCE.test(resource)) {
|
|
207
|
+
return {
|
|
208
|
+
ok: false,
|
|
209
|
+
error: "An Azure resource name has letters, digits and hyphens, and starts and ends with a letter or digit."
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
if (!input.openai && !input.foundry) {
|
|
213
|
+
return { ok: false, error: "Choose Azure OpenAI, Claude on Foundry or both." };
|
|
214
|
+
}
|
|
215
|
+
const taken = new Set(input.takenIds ?? []);
|
|
216
|
+
const entries = [];
|
|
217
|
+
const credential = input.credential ? { credential: { ...input.credential } } : {};
|
|
218
|
+
if (input.openai) {
|
|
219
|
+
const map = deploymentModelMap(input.openai.deployments);
|
|
220
|
+
if (!map.ok) return map;
|
|
221
|
+
const first = Object.values(map.modelMap)[0];
|
|
222
|
+
if (!first) return { ok: false, error: "Add at least one Azure OpenAI deployment." };
|
|
223
|
+
const apiVersion = input.openai.apiVersion.trim();
|
|
224
|
+
if (!apiVersion) return { ok: false, error: "Enter the Azure OpenAI api_version." };
|
|
225
|
+
const id = freeId(AZURE_OPENAI_PRESET, resource, taken);
|
|
226
|
+
taken.add(id);
|
|
227
|
+
entries.push({
|
|
228
|
+
id,
|
|
229
|
+
body: {
|
|
230
|
+
preset: AZURE_OPENAI_PRESET,
|
|
231
|
+
label: "Azure OpenAI",
|
|
232
|
+
params: { resource, deployment: first, api_version: apiVersion },
|
|
233
|
+
model_map: map.modelMap,
|
|
234
|
+
...credential
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
if (input.foundry) {
|
|
239
|
+
const map = deploymentModelMap(input.foundry.deployments);
|
|
240
|
+
if (!map.ok) return map;
|
|
241
|
+
const id = freeId(FOUNDRY_PRESET, resource, taken);
|
|
242
|
+
taken.add(id);
|
|
243
|
+
entries.push({
|
|
244
|
+
id,
|
|
245
|
+
body: {
|
|
246
|
+
preset: FOUNDRY_PRESET,
|
|
247
|
+
label: "Claude on Foundry",
|
|
248
|
+
params: { resource },
|
|
249
|
+
...Object.keys(map.modelMap).length > 0 ? { model_map: map.modelMap } : {},
|
|
250
|
+
...credential
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
return { ok: true, entries };
|
|
255
|
+
}
|