@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.
Files changed (88) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +52 -9
  3. package/dist/module.json +1 -1
  4. package/dist/runtime/ai-sdk/index.d.ts +33 -26
  5. package/dist/runtime/ai-sdk/index.js +34 -16
  6. package/dist/runtime/components/ForgeProvidersPanel.d.vue.ts +13 -0
  7. package/dist/runtime/components/ForgeProvidersPanel.vue +461 -78
  8. package/dist/runtime/components/ForgeProvidersPanel.vue.d.ts +13 -0
  9. package/dist/runtime/editor/index.d.ts +55 -16
  10. package/dist/runtime/editor/index.js +82 -40
  11. package/dist/runtime/model/selector.d.ts +16 -0
  12. package/dist/runtime/model/selector.js +12 -0
  13. package/dist/runtime/server/credential-store.d.ts +16 -4
  14. package/dist/runtime/server/credential-store.js +3 -4
  15. package/dist/runtime/server/env-layers.d.ts +1 -1
  16. package/dist/runtime/server/env-layers.js +3 -3
  17. package/dist/runtime/server/index.d.ts +9 -8
  18. package/dist/runtime/server/index.js +3 -3
  19. package/dist/runtime/server/provider-discovery.d.ts +28 -8
  20. package/dist/runtime/server/provider-discovery.js +57 -33
  21. package/dist/runtime/server/provider-layers.d.ts +1 -1
  22. package/dist/runtime/server/provider-layers.js +3 -5
  23. package/dist/runtime/server/provider-service.d.ts +26 -2
  24. package/dist/runtime/server/provider-service.js +16 -1
  25. package/dist/runtime/server/provider-write.d.ts +26 -10
  26. package/dist/runtime/server/provider-write.js +75 -7
  27. package/dist/runtime/server/voice-provider.d.ts +61 -0
  28. package/dist/runtime/server/voice-provider.js +31 -0
  29. package/dist/runtime/shared/data-zone.d.ts +68 -0
  30. package/dist/runtime/shared/data-zone.js +79 -0
  31. package/dist/runtime/shared/harness-providers.d.ts +42 -3
  32. package/dist/runtime/shared/harness-providers.js +31 -8
  33. package/dist/runtime/shared/harnesses.d.ts +7 -0
  34. package/dist/runtime/shared/harnesses.js +18 -0
  35. package/dist/runtime/shared/provider-accounts.d.ts +215 -0
  36. package/dist/runtime/shared/provider-accounts.js +255 -0
  37. package/dist/runtime/shared/provider-registry.d.ts +19 -182
  38. package/dist/runtime/shared/provider-registry.js +25 -65
  39. package/dist/runtime/vue/index.d.ts +2 -0
  40. package/dist/runtime/vue/index.js +2 -0
  41. package/dist/runtime/vue/providers-panel-model.d.ts +98 -5
  42. package/dist/runtime/vue/providers-panel-model.js +13 -1
  43. package/dist/runtime/vue/useProvidersPanel.d.ts +66 -5
  44. package/dist/runtime/vue/useProvidersPanel.js +417 -29
  45. package/dist/runtime/{server → workspaces}/harness-settings.d.ts +51 -13
  46. package/dist/runtime/{server → workspaces}/harness-settings.js +93 -9
  47. package/dist/runtime/workspaces/index.d.ts +16 -0
  48. package/dist/runtime/workspaces/index.js +5 -0
  49. package/dist/runtime/workspaces/language-model.d.ts +45 -0
  50. package/dist/runtime/workspaces/language-model.js +26 -0
  51. package/dist/runtime/workspaces/model-endpoint.d.ts +38 -0
  52. package/dist/runtime/workspaces/model-endpoint.js +54 -0
  53. package/dist/runtime/workspaces/provider-settings.d.ts +35 -0
  54. package/dist/runtime/{server → workspaces}/provider-settings.js +0 -31
  55. package/dist/runtime/{server → workspaces}/settings-defaults.d.ts +1 -1
  56. package/dist/runtime/{shared → workspaces}/settings-types.d.ts +11 -4
  57. package/package.json +11 -2
  58. package/src/runtime/ai-sdk/index.ts +88 -47
  59. package/src/runtime/components/ForgeProvidersPanel.vue +477 -78
  60. package/src/runtime/editor/index.ts +158 -63
  61. package/src/runtime/model/selector.ts +31 -0
  62. package/src/runtime/server/credential-store.ts +20 -10
  63. package/src/runtime/server/env-layers.ts +4 -4
  64. package/src/runtime/server/index.ts +9 -8
  65. package/src/runtime/server/provider-discovery.ts +114 -48
  66. package/src/runtime/server/provider-layers.ts +6 -6
  67. package/src/runtime/server/provider-service.ts +53 -1
  68. package/src/runtime/server/provider-write.ts +132 -18
  69. package/src/runtime/server/voice-provider.ts +110 -0
  70. package/src/runtime/shared/data-zone.ts +179 -0
  71. package/src/runtime/shared/harness-providers.ts +78 -7
  72. package/src/runtime/shared/harnesses.ts +25 -0
  73. package/src/runtime/shared/provider-accounts.ts +526 -0
  74. package/src/runtime/shared/provider-registry.ts +82 -256
  75. package/src/runtime/vue/index.ts +2 -0
  76. package/src/runtime/vue/providers-panel-model.ts +123 -5
  77. package/src/runtime/vue/useProvidersPanel.ts +589 -37
  78. package/src/runtime/{server → workspaces}/harness-settings.ts +166 -22
  79. package/src/runtime/workspaces/index.ts +17 -0
  80. package/src/runtime/workspaces/language-model.ts +82 -0
  81. package/src/runtime/workspaces/model-endpoint.ts +100 -0
  82. package/src/runtime/workspaces/provider-settings.ts +96 -0
  83. package/src/runtime/{server → workspaces}/settings-defaults.ts +1 -1
  84. package/src/runtime/{shared → workspaces}/settings-types.ts +11 -4
  85. package/dist/runtime/server/provider-settings.d.ts +0 -78
  86. package/src/runtime/server/provider-settings.ts +0 -192
  87. /package/dist/runtime/{server → workspaces}/settings-defaults.js +0 -0
  88. /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
- /** How many registry entries a harness can run at once (`@skaile/workspaces` `HarnessInfo.providerPlurality`). */
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 resolveHarnessProviders(harness, candidates, selection) {
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 eligible = candidates.filter(
10
- (candidate) => isProviderEligibleForHarness(harness, candidate)
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
+ }