@skaile/forge-settings 1.4.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +34 -11
  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 +2 -2
  7. package/dist/runtime/components/ForgeProvidersPanel.vue.d.ts +2 -2
  8. package/dist/runtime/editor/index.d.ts +55 -16
  9. package/dist/runtime/editor/index.js +82 -40
  10. package/dist/runtime/model/selector.d.ts +16 -0
  11. package/dist/runtime/model/selector.js +12 -0
  12. package/dist/runtime/server/credential-store.d.ts +16 -4
  13. package/dist/runtime/server/credential-store.js +3 -4
  14. package/dist/runtime/server/env-layers.d.ts +1 -1
  15. package/dist/runtime/server/env-layers.js +3 -3
  16. package/dist/runtime/server/index.d.ts +7 -8
  17. package/dist/runtime/server/index.js +1 -3
  18. package/dist/runtime/server/provider-discovery.d.ts +28 -8
  19. package/dist/runtime/server/provider-discovery.js +57 -33
  20. package/dist/runtime/server/provider-layers.d.ts +1 -1
  21. package/dist/runtime/server/provider-layers.js +3 -5
  22. package/dist/runtime/server/provider-service.d.ts +2 -2
  23. package/dist/runtime/server/provider-write.d.ts +1 -1
  24. package/dist/runtime/server/provider-write.js +3 -3
  25. package/dist/runtime/server/voice-provider.d.ts +61 -0
  26. package/dist/runtime/server/voice-provider.js +31 -0
  27. package/dist/runtime/shared/data-zone.d.ts +28 -26
  28. package/dist/runtime/shared/data-zone.js +4 -4
  29. package/dist/runtime/shared/harness-providers.d.ts +7 -7
  30. package/dist/runtime/shared/provider-accounts.d.ts +1 -1
  31. package/dist/runtime/shared/provider-registry.d.ts +19 -220
  32. package/dist/runtime/shared/provider-registry.js +25 -81
  33. package/dist/runtime/vue/providers-panel-model.d.ts +4 -4
  34. package/dist/runtime/{server → workspaces}/harness-settings.d.ts +3 -3
  35. package/dist/runtime/workspaces/index.d.ts +16 -0
  36. package/dist/runtime/workspaces/index.js +5 -0
  37. package/dist/runtime/workspaces/language-model.d.ts +45 -0
  38. package/dist/runtime/workspaces/language-model.js +26 -0
  39. package/dist/runtime/workspaces/model-endpoint.d.ts +38 -0
  40. package/dist/runtime/workspaces/model-endpoint.js +54 -0
  41. package/dist/runtime/workspaces/provider-settings.d.ts +35 -0
  42. package/dist/runtime/{server → workspaces}/provider-settings.js +0 -31
  43. package/dist/runtime/{server → workspaces}/settings-defaults.d.ts +1 -1
  44. package/dist/runtime/{shared → workspaces}/settings-types.d.ts +4 -4
  45. package/package.json +13 -4
  46. package/src/runtime/ai-sdk/index.ts +88 -47
  47. package/src/runtime/components/ForgeProvidersPanel.vue +2 -2
  48. package/src/runtime/editor/index.ts +158 -63
  49. package/src/runtime/model/selector.ts +31 -0
  50. package/src/runtime/server/credential-store.ts +20 -10
  51. package/src/runtime/server/env-layers.ts +4 -4
  52. package/src/runtime/server/index.ts +7 -8
  53. package/src/runtime/server/provider-discovery.ts +114 -48
  54. package/src/runtime/server/provider-layers.ts +6 -6
  55. package/src/runtime/server/provider-service.ts +2 -2
  56. package/src/runtime/server/provider-write.ts +3 -3
  57. package/src/runtime/server/voice-provider.ts +110 -0
  58. package/src/runtime/shared/data-zone.ts +40 -33
  59. package/src/runtime/shared/harness-providers.ts +7 -7
  60. package/src/runtime/shared/provider-accounts.ts +1 -1
  61. package/src/runtime/shared/provider-registry.ts +82 -314
  62. package/src/runtime/vue/providers-panel-model.ts +5 -5
  63. package/src/runtime/vue/useProvidersPanel.ts +5 -2
  64. package/src/runtime/{server → workspaces}/harness-settings.ts +3 -3
  65. package/src/runtime/workspaces/index.ts +17 -0
  66. package/src/runtime/workspaces/language-model.ts +82 -0
  67. package/src/runtime/workspaces/model-endpoint.ts +100 -0
  68. package/src/runtime/workspaces/provider-settings.ts +96 -0
  69. package/src/runtime/{server → workspaces}/settings-defaults.ts +1 -1
  70. package/src/runtime/{shared → workspaces}/settings-types.ts +4 -4
  71. package/dist/runtime/server/provider-settings.d.ts +0 -78
  72. package/src/runtime/server/provider-settings.ts +0 -192
  73. /package/dist/runtime/{server → workspaces}/harness-settings.js +0 -0
  74. /package/dist/runtime/{server → workspaces}/settings-defaults.js +0 -0
  75. /package/dist/runtime/{shared → workspaces}/settings-types.js +0 -0
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Small shared pieces of registry model resolution: the provider a selector
3
+ * names and the credential-store options a resolution carries.
4
+ */
5
+ import type { CredentialStoreOptions } from "../server/credential-store.js";
6
+ /**
7
+ * The provider id a `<providerId>/<modelId>` selector names: the segment before
8
+ * the first `/`. `undefined` for an `@role` selector or a bare model id.
9
+ */
10
+ export declare function selectorProviderId(selector: string): string | undefined;
11
+ /** The policy and local store a credential resolution uses. */
12
+ export type CredentialAccessOptions = Pick<CredentialStoreOptions, "policy" | "path" | "key">;
13
+ /** `env` plus whichever of `policy`, `path` and `key` are set, without `undefined` keys. */
14
+ export declare function credentialStoreOptions(env: Record<string, string | undefined>, { policy, path, key }: CredentialAccessOptions): CredentialStoreOptions & {
15
+ env: Record<string, string | undefined>;
16
+ };
@@ -0,0 +1,12 @@
1
+ export function selectorProviderId(selector) {
2
+ const slash = selector.indexOf("/");
3
+ return !selector.startsWith("@") && slash > 0 ? selector.slice(0, slash) : void 0;
4
+ }
5
+ export function credentialStoreOptions(env, { policy, path, key }) {
6
+ return {
7
+ env,
8
+ ...policy ? { policy } : {},
9
+ ...path ? { path } : {},
10
+ ...key ? { key } : {}
11
+ };
12
+ }
@@ -24,6 +24,10 @@
24
24
  *
25
25
  * Blob format: `enc:<base64(iv[16] + tag[16] + ciphertext)>`.
26
26
  *
27
+ * Store path: no implicit home. The caller passes `path` or sets
28
+ * `FORGE_CREDENTIAL_STORE` in the environment it hands over; see
29
+ * {@link credentialStorePath}.
30
+ *
27
31
  * Key format: `FORGE_CREDENTIAL_KEY` must be a 64-char hex string (32 bytes).
28
32
  */
29
33
  import type { AIProviderEntry, AIProviderRegistryView } from "../shared/provider-registry.js";
@@ -72,7 +76,7 @@ export declare function decryptCredential(ciphertext: string, hexKey: string, re
72
76
  export interface CredentialStoreOptions {
73
77
  /** 64-char hex key; defaults to `FORGE_CREDENTIAL_KEY` of the environment. */
74
78
  key?: string;
75
- /** Store file; defaults to `FORGE_CREDENTIAL_STORE` or `<skaile home>/forge-credentials.json`. */
79
+ /** Store file; defaults to the environment's `FORGE_CREDENTIAL_STORE`. Required for keyring operations. */
76
80
  path?: string;
77
81
  /**
78
82
  * Environment `env:` references (and the key and store path variables) read
@@ -87,11 +91,19 @@ export interface CredentialStoreOptions {
87
91
  */
88
92
  policy?: CredentialPolicy;
89
93
  }
94
+ /** What a keyring operation throws when neither a path nor `FORGE_CREDENTIAL_STORE` names the store. */
95
+ export declare const CREDENTIAL_STORE_PATH_REQUIRED: "FORGE_CREDENTIAL_STORE or credential store path is required";
90
96
  /**
91
- * Path of the local keyring file.
97
+ * Path of the local keyring file: `options.path`, else the selected
98
+ * environment's `FORGE_CREDENTIAL_STORE`.
99
+ *
100
+ * There is no implicit home: the host decides where its secrets live. A
101
+ * Workspaces host passes `<skaile home>/forge-credentials.json` (see
102
+ * `@skaile/forge-settings/workspaces`). Only keyring operations ask for the
103
+ * path, so env-only and keyless providers need none.
92
104
  *
93
- * Lives beside the user's `settings.json` so it follows `$SKAILE_HOME`, and is
94
- * overridable per app with `FORGE_CREDENTIAL_STORE`.
105
+ * @throws Error `FORGE_CREDENTIAL_STORE or credential store path is required`
106
+ * when neither is given.
95
107
  */
96
108
  export declare function credentialStorePath(options?: CredentialStoreOptions): string;
97
109
  /**
@@ -1,14 +1,12 @@
1
1
  import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
2
2
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
- import { dirname, join } from "node:path";
4
- import { globalSettingsPath } from "@skaile/workspaces/sdk/core";
3
+ import { dirname } from "node:path";
5
4
  import { isEntryEnabled, parseCredentialRef, registryEntries } from "../shared/provider-registry.js";
6
5
  import {
7
6
  credentialRefRefusal,
8
7
  entryPolicyRefusal
9
8
  } from "./credential-policy.js";
10
9
  export const CREDENTIAL_PREFIX = "enc:";
11
- const STORE_FILENAME = "forge-credentials.json";
12
10
  const ALGORITHM = "aes-256-gcm";
13
11
  const IV_LENGTH = 16;
14
12
  const TAG_LENGTH = 16;
@@ -85,11 +83,12 @@ export function decryptCredential(ciphertext, hexKey, ref) {
85
83
  function envOf(options) {
86
84
  return options.env ?? (options.policy ? {} : process.env);
87
85
  }
86
+ export const CREDENTIAL_STORE_PATH_REQUIRED = "FORGE_CREDENTIAL_STORE or credential store path is required";
88
87
  export function credentialStorePath(options = {}) {
89
88
  if (options.path) return options.path;
90
89
  const fromEnv = envOf(options).FORGE_CREDENTIAL_STORE;
91
90
  if (fromEnv) return fromEnv;
92
- return join(dirname(globalSettingsPath()), STORE_FILENAME);
91
+ throw new Error(CREDENTIAL_STORE_PATH_REQUIRED);
93
92
  }
94
93
  function requireKey(options) {
95
94
  const key = options.key ?? envOf(options).FORGE_CREDENTIAL_KEY;
@@ -13,8 +13,8 @@
13
13
  * browser-safe `utils/env-vars.ts` and is re-exported here, so client and
14
14
  * server refuse the same names from one source.
15
15
  */
16
- import type { AIProviderRegistryView } from "../shared/provider-registry.js";
17
16
  import { type EnvLayerId, type EnvSettingsDocument, type EnvSettingsUpdate } from "../shared/env-vars.js";
17
+ import type { AIProviderRegistryView } from "../shared/provider-registry.js";
18
18
  export * from "../shared/env-vars.js";
19
19
  /**
20
20
  * Parse `.env` text: blank and `#` lines skipped, optional `export ` prefix,
@@ -1,12 +1,12 @@
1
1
  import { chmodSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
- import { AI_PROVIDER_PRESETS } from "@skaile/workspaces/sdk/bridge";
4
- import { isPlainObject } from "../shared/guards.js";
5
- import { parseCredentialRef, registryEntries } from "../shared/provider-registry.js";
3
+ import { AI_PROVIDER_PRESETS } from "@skaile/provider-registry/catalog";
6
4
  import {
7
5
  ENV_LAYER_ORDER,
8
6
  validateEnvVarName
9
7
  } from "../shared/env-vars.js";
8
+ import { isPlainObject } from "../shared/guards.js";
9
+ import { parseCredentialRef, registryEntries } from "../shared/provider-registry.js";
10
10
  export * from "../shared/env-vars.js";
11
11
  const ASSIGNMENT = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_.-]*)\s*=(.*)$/;
12
12
  const NEEDS_QUOTES = /[\s#"'=\\]/;
@@ -1,9 +1,11 @@
1
1
  /**
2
- * `@skaile/forge-settings/server` — provider and settings operations for a
3
- * host server: the registry vocabulary and layer editing, the credential
4
- * store, env layers, harness settings, model discovery.
2
+ * `@skaile/forge-settings/server` — provider operations for a host server: the
3
+ * registry vocabulary and layer editing, the credential store, env layers,
4
+ * model discovery and voice-entry selection.
5
5
  *
6
- * Imports no Vue, no editor, no AI SDK and no agent session code. Storage,
6
+ * Imports no Vue, no editor, no AI SDK, no agent session code and no Skaile
7
+ * Workspaces: the Workspaces settings document, harness settings and project
8
+ * registry loading live in `@skaile/forge-settings/workspaces`. Storage,
7
9
  * authentication and workspace permissions stay with the host.
8
10
  */
9
11
  export * from "../shared/data-zone.js";
@@ -12,15 +14,12 @@ export * from "../shared/harnesses.js";
12
14
  export * from "../shared/model-roles.js";
13
15
  export * from "../shared/provider-accounts.js";
14
16
  export * from "../shared/provider-registry.js";
15
- export type { DomainConfig, SkaileSettings } from "../shared/settings-types.js";
16
17
  export * from "./credential-policy.js";
17
18
  export * from "./credential-store.js";
18
19
  export * from "./env.js";
19
20
  export * from "./env-layers.js";
20
- export * from "./harness-settings.js";
21
21
  export * from "./provider-discovery.js";
22
22
  export * from "./provider-layers.js";
23
23
  export * from "./provider-service.js";
24
- export * from "./provider-settings.js";
25
24
  export * from "./provider-write.js";
26
- export * from "./settings-defaults.js";
25
+ export * from "./voice-provider.js";
@@ -8,10 +8,8 @@ export * from "./credential-policy.js";
8
8
  export * from "./credential-store.js";
9
9
  export * from "./env.js";
10
10
  export * from "./env-layers.js";
11
- export * from "./harness-settings.js";
12
11
  export * from "./provider-discovery.js";
13
12
  export * from "./provider-layers.js";
14
13
  export * from "./provider-service.js";
15
- export * from "./provider-settings.js";
16
14
  export * from "./provider-write.js";
17
- export * from "./settings-defaults.js";
15
+ export * from "./voice-provider.js";
@@ -5,11 +5,12 @@
5
5
  * `model_map`; this module asks the provider's REST API. The two are different
6
6
  * facts and the result says which one it is:
7
7
  *
8
- * - `live` — the provider answered; `models` is its list.
9
- * - `static` — the live request failed or the entry has no key, and `models` is
10
- * the offline catalog of its preset. It is a fallback, never a live answer.
11
- * - `unsupported` — the entry has no catalog preset, so there is nothing to ask;
12
- * a host lets the admin type a model id.
8
+ * - `live` — the provider answered; `models` is its list (possibly empty).
9
+ * - `static` — the live request failed or the entry speaks no models list, and
10
+ * `models` is the offline catalog of the entry's own preset. It is a
11
+ * labelled fallback, never a live answer and never an inference destination.
12
+ * - `unsupported` — the entry speaks no models list and has no catalog; a host
13
+ * lets the admin type a model id.
13
14
  * - `error` — nothing usable. The message is scrubbed.
14
15
  *
15
16
  * The error text is built here, never forwarded: a remote response body is
@@ -55,9 +56,12 @@ export interface DiscoverProviderModelsOptions {
55
56
  policy?: CredentialPolicy;
56
57
  /** Extra strings to remove from error text, besides the secrets this call resolves. */
57
58
  redact?: readonly string[];
58
- /** Local encrypted store for `keyring:` references. */
59
- key?: string;
59
+ /** Local encrypted store for `keyring:` references; else `FORGE_CREDENTIAL_STORE` of `env`. */
60
60
  path?: string;
61
+ /** 64-char hex key of the local store; else `FORGE_CREDENTIAL_KEY` of `env`. */
62
+ key?: string;
63
+ /** Cancels a live request; the result is then an `error` / `static` one. */
64
+ signal?: AbortSignal;
61
65
  }
62
66
  /**
63
67
  * Remove every occurrence of each secret (verbatim, URL-encoded and
@@ -67,7 +71,23 @@ export declare function scrubSecrets(text: string, secrets: readonly (string | u
67
71
  /**
68
72
  * Ask one registry entry's provider for its models.
69
73
  *
74
+ * Two ways to ask, both with the credential resolved here under the policy:
75
+ *
76
+ * - A catalog-preset entry with a resolved key and no `discovery` block of its
77
+ * own uses the preset's list parser (`fetchProviderModels`), which filters
78
+ * the vendor's list the way that vendor needs.
79
+ * - Every other entry — a custom endpoint, a keyless local server, an entry
80
+ * that declares `discovery` — asks its own `base_url` with its own headers
81
+ * and credential carrier (`fetchEntryModels`), exactly as inference would.
82
+ * No preset and no secret are required: `credential.kind: none` queries
83
+ * without `Authorization`.
84
+ *
85
+ * A successful empty list is `live` and empty. Only a preset entry has a
86
+ * `static` fallback: its own catalog, labelled as such, never another
87
+ * provider and never an inference destination.
88
+ *
70
89
  * Never throws for a provider-side failure: that is the `error` / `static`
71
- * result. A host maps `error` to its own status.
90
+ * result. A host maps `error` to its own status. A refused or unresolvable
91
+ * credential sends no request at all.
72
92
  */
73
93
  export declare function discoverProviderModels(providerId: string, registry: AIProviderRegistryView, options: DiscoverProviderModelsOptions): Promise<ModelDiscoveryResult>;
@@ -1,5 +1,10 @@
1
- import { resolveEntryBaseUrl } from "@skaile/workspaces/core";
2
- import { fetchProviderModels, getStaticModels } from "@skaile/workspaces/sdk/bridge";
1
+ import { resolveEntryBaseUrl } from "@skaile/provider-registry";
2
+ import { entrySurface, getStaticModels } from "@skaile/provider-registry/catalog";
3
+ import {
4
+ fetchEntryModels,
5
+ fetchProviderModels
6
+ } from "@skaile/provider-registry/discovery";
7
+ import { credentialStoreOptions } from "../model/selector.js";
3
8
  import { isEntryEnabled, registryEntries } from "../shared/provider-registry.js";
4
9
  import { entryPolicyRefusal } from "./credential-policy.js";
5
10
  import { resolveEntryCredential } from "./credential-store.js";
@@ -18,6 +23,32 @@ export function scrubSecrets(text, secrets) {
18
23
  }
19
24
  return out.length > MAX_ERROR_LENGTH ? `${out.slice(0, MAX_ERROR_LENGTH)}\u2026` : out;
20
25
  }
26
+ function entryListing(entry) {
27
+ const type = entry.discovery?.type;
28
+ if (type === "none") return { ok: false, reason: "this entry declares no model discovery" };
29
+ if (type === "openai-models-list" || type === "anthropic-models-list") return { ok: true };
30
+ if (type !== void 0) return { ok: false, reason: `discovery type '${type}' is not supported` };
31
+ const surface = entrySurface(entry);
32
+ if (surface === "openai-completions" || surface === "openai-responses" || surface === "anthropic-messages") {
33
+ return { ok: true };
34
+ }
35
+ return { ok: false, reason: `surface '${surface ?? "unknown"}' has no models list` };
36
+ }
37
+ const CANCELLED = {
38
+ ok: false,
39
+ models: [],
40
+ error: "models list request was cancelled",
41
+ source: "api"
42
+ };
43
+ function untilAborted(start, signal) {
44
+ if (!signal) return start();
45
+ if (signal.aborted) return Promise.resolve(CANCELLED);
46
+ return new Promise((resolve, reject) => {
47
+ const onAbort = () => resolve(CANCELLED);
48
+ signal.addEventListener("abort", onAbort, { once: true });
49
+ start().finally(() => signal.removeEventListener("abort", onAbort)).then(resolve, reject);
50
+ });
51
+ }
21
52
  export async function discoverProviderModels(providerId, registry, options) {
22
53
  const match = registryEntries(registry).find(([id]) => id === providerId);
23
54
  if (!match)
@@ -29,43 +60,36 @@ export async function discoverProviderModels(providerId, registry, options) {
29
60
  if (!isEntryEnabled(entry)) return { status: "error", providerId, error: "entry is disabled" };
30
61
  const refusal = entryPolicyRefusal(entry, options.policy);
31
62
  if (refusal) return { status: "error", providerId, error: refusal };
32
- const credential = resolveEntryCredential(entry, {
33
- env: options.env,
34
- ...options.policy ? { policy: options.policy } : {},
35
- ...options.key ? { key: options.key } : {},
36
- ...options.path ? { path: options.path } : {}
37
- });
63
+ const credential = resolveEntryCredential(entry, credentialStoreOptions(options.env, options));
38
64
  const secrets = [credential.value, ...options.redact ?? []];
39
65
  const scrub = (text) => scrubSecrets(text, secrets);
40
- if (!entry.preset) {
41
- return {
42
- status: "unsupported",
43
- providerId,
44
- reason: "this entry names no catalog preset, so its models cannot be listed; enter a model id"
45
- };
46
- }
47
66
  if (credential.error && !credential.value) {
48
67
  return { status: "error", providerId, error: scrub(credential.error) };
49
68
  }
50
- const staticList = getStaticModels(entry.preset).map(({ id, name }) => ({ id, name }));
51
- if (!credential.value) {
52
- return staticList.length > 0 ? {
53
- status: "static",
54
- providerId,
55
- models: staticList,
56
- reason: "no credential to query the provider with"
57
- } : {
58
- status: "unsupported",
59
- providerId,
60
- reason: "no credential, and no offline catalog for this preset"
61
- };
69
+ const staticList = entry.preset ? getStaticModels(entry.preset).map(({ id, name }) => ({ id, name })) : [];
70
+ let live;
71
+ if (entry.preset && !entry.discovery && credential.value) {
72
+ const baseUrl = resolveEntryBaseUrl(entry);
73
+ const apiKey = credential.value;
74
+ live = await untilAborted(
75
+ () => fetchProviderModels(entry.preset, apiKey, {
76
+ ...baseUrl ? { baseUrl } : {},
77
+ env: options.env
78
+ }),
79
+ options.signal
80
+ );
81
+ } else {
82
+ const listing = entryListing(entry);
83
+ if (!listing.ok) {
84
+ return staticList.length > 0 ? { status: "static", providerId, models: staticList, reason: listing.reason } : { status: "unsupported", providerId, reason: `${listing.reason}; enter a model id` };
85
+ }
86
+ live = await fetchEntryModels(entry, {
87
+ env: {},
88
+ ...credential.value !== void 0 ? { secret: credential.value } : {},
89
+ ...options.signal ? { signal: options.signal } : {}
90
+ });
62
91
  }
63
- const baseUrl = resolveEntryBaseUrl(entry);
64
- const live = await fetchProviderModels(entry.preset, credential.value, {
65
- ...baseUrl ? { baseUrl } : {},
66
- env: options.env
67
- });
68
- if (live.ok && live.models.length > 0) {
92
+ if (live.ok) {
69
93
  return {
70
94
  status: "live",
71
95
  providerId,
@@ -7,7 +7,7 @@
7
7
  * therefore read the raw `providers` map and write it back unchanged apart
8
8
  * from the entry being edited.
9
9
  */
10
- import { type AIProviderPreset } from "@skaile/workspaces/sdk/bridge";
10
+ import { type AIProviderPreset } from "@skaile/provider-registry/catalog";
11
11
  /**
12
12
  * The file a write to the layer at `path` goes to: the `.yaml` spelling when
13
13
  * only that one exists, so an edit never shadows a hand-written document.
@@ -1,11 +1,9 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
- import {
3
- PROVIDERS_FILENAME,
4
- saveProviderRegistryFile
5
- } from "@skaile/workspaces/core";
6
- import { AI_PROVIDER_PRESETS } from "@skaile/workspaces/sdk/bridge";
2
+ import { AI_PROVIDER_PRESETS } from "@skaile/provider-registry/catalog";
3
+ import { saveProviderRegistryFile } from "@skaile/provider-registry/files";
7
4
  import { parse as parseYaml } from "yaml";
8
5
  import { isPlainObject } from "../shared/guards.js";
6
+ const PROVIDERS_FILENAME = "providers.yml";
9
7
  const PROVIDERS_FILENAME_ALIAS = "providers.yaml";
10
8
  export function providerLayerDocument(path) {
11
9
  if (existsSync(path)) return path;
@@ -18,7 +18,7 @@
18
18
  * `model_map`/`capabilities`; harness-backed discovery is
19
19
  * `listModelsForHarness(harnessId, registry)` in the runtime.
20
20
  */
21
- import type { AIProviderCapabilities, AIProviderCredentialKind, AIProviderKind, AIProviderRegistryView, AIProviderSurface, AIProviderTransport, AIProviderVoiceParams, DataZone } from "../shared/provider-registry.js";
21
+ import type { AIProviderCapabilities, AIProviderCredentialKind, AIProviderDataZone, AIProviderKind, AIProviderRegistryView, AIProviderSurface, AIProviderTransport, AIProviderVoiceParams } from "../shared/provider-registry.js";
22
22
  import { type CredentialSource, type CredentialStoreOptions } from "./credential-store.js";
23
23
  /** One registry entry, as a settings UI needs to see it. */
24
24
  export interface ProviderInfo {
@@ -72,7 +72,7 @@ export interface ProviderInfo {
72
72
  /** Frozen by the control plane: a lower layer cannot change or disable it. */
73
73
  locked: boolean;
74
74
  /** Where the entry processes data: its `data_zone`, else its preset's, else `transfer`. */
75
- dataZone: DataZone;
75
+ dataZone: AIProviderDataZone;
76
76
  /** The entry's `eu_processing` switch (an `eu_option` entry counts as EU only with it on). */
77
77
  euProcessing: boolean;
78
78
  /**
@@ -22,7 +22,7 @@
22
22
  * Auth stays in the app: it builds the {@link ProviderWriteScope} (paths, env,
23
23
  * lock state), asks {@link providerScopeWritable}, and answers 403 itself.
24
24
  */
25
- import { type AIProviderPreset } from "@skaile/workspaces/sdk/bridge";
25
+ import { type AIProviderPreset } from "@skaile/provider-registry/catalog";
26
26
  import type { AIProviderRegistry } from "../shared/provider-registry.js";
27
27
  import { type CredentialPolicy } from "./credential-policy.js";
28
28
  /**
@@ -1,15 +1,15 @@
1
1
  import {
2
2
  isSafeProviderId,
3
- loadProviderRegistryFile,
4
3
  mergeRegistries,
5
4
  parseCredentialRef,
6
5
  validateMergedRegistry,
7
6
  validateRegistry
8
- } from "@skaile/workspaces/core";
7
+ } from "@skaile/provider-registry";
9
8
  import {
10
9
  AI_PROVIDER_PRESETS,
11
10
  expandPreset
12
- } from "@skaile/workspaces/sdk/bridge";
11
+ } from "@skaile/provider-registry/catalog";
12
+ import { loadProviderRegistryFile } from "@skaile/provider-registry/files";
13
13
  import { isDataZone } from "../shared/data-zone.js";
14
14
  import { isPlainObject } from "../shared/guards.js";
15
15
  import { missingRequiredParams } from "../shared/provider-accounts.js";
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Voice selection: which `kind: stt|tts` registry entry serves a request and
3
+ * with which parameters.
4
+ *
5
+ * `sttProvider`/`ttsProvider` are registry ids of `kind: stt|tts` entries; the
6
+ * endpoint and the voice parameters live on the entry, and
7
+ * `sttLanguage`/`ttsVoice` are per-request overrides of two of those
8
+ * parameters. Resolving all of that used to be three apps' worth of hardcoded
9
+ * endpoints and voice ids.
10
+ */
11
+ import type { AIProviderRegistryView, AIProviderVoiceEntry, AIProviderVoiceKind, AIProviderVoiceParams } from "../shared/provider-registry.js";
12
+ /**
13
+ * Settings fields that name a voice entry and override its parameters — the
14
+ * same field names as the Workspaces `settings.json` document, so a
15
+ * `SkaileSettings` (`@skaile/forge-settings/workspaces`) can be passed as is.
16
+ */
17
+ export interface VoiceSettings {
18
+ /** Registry id of the `kind: stt` entry. */
19
+ sttProvider?: string;
20
+ /** Registry id of the `kind: tts` entry. */
21
+ ttsProvider?: string;
22
+ /** Overrides the stt entry's `language` parameter. */
23
+ sttLanguage?: string;
24
+ /** Overrides the tts entry's `voice_id` parameter. */
25
+ ttsVoice?: string;
26
+ }
27
+ /** The `kind: stt|tts` entry a request runs against, with its parameters resolved. */
28
+ export interface VoiceProviderSelection {
29
+ /** Registry id of the entry. */
30
+ providerId: string;
31
+ entry: AIProviderVoiceEntry;
32
+ /** Endpoint from the entry; absent when the entry inherits its preset's default. */
33
+ baseUrl?: string;
34
+ /**
35
+ * The entry's `params` with overrides applied — Deepgram `{model, language}`,
36
+ * ElevenLabs `{voice_id, output_format}`. This is the only place a forge app
37
+ * should read voice defaults from.
38
+ */
39
+ params: AIProviderVoiceParams;
40
+ /** True when settings named no usable entry and registry order decided. */
41
+ fallback: boolean;
42
+ /** Non-fatal notes, e.g. settings naming an entry the registry does not have. */
43
+ warnings: string[];
44
+ }
45
+ /**
46
+ * Resolve which registry entry serves speech-to-text or text-to-speech.
47
+ *
48
+ * Selection is `settings.sttProvider`/`settings.ttsProvider`, falling back to
49
+ * the first enabled entry of that kind in registry order when the setting is
50
+ * absent or names an entry that is gone — a missing setting must not silence an
51
+ * app that has a perfectly good entry configured.
52
+ *
53
+ * Parameter precedence, lowest first: the entry's `params`, then the settings
54
+ * overrides (`sttLanguage` → `language`, `ttsVoice` → `voice_id`), then
55
+ * `overrides` from the request. The per-request override is the contract
56
+ * forge-concept already honoured and the other apps silently dropped.
57
+ *
58
+ * @returns The selection, or `undefined` when the registry has no enabled entry
59
+ * of that kind.
60
+ */
61
+ export declare function resolveVoiceProvider(registry: AIProviderRegistryView, kind: AIProviderVoiceKind, settings?: VoiceSettings, overrides?: AIProviderVoiceParams): VoiceProviderSelection | undefined;
@@ -0,0 +1,31 @@
1
+ import {
2
+ entriesOfKind,
3
+ selectVoiceEntryId,
4
+ VOICE_OVERRIDE_PARAM
5
+ } from "../shared/provider-registry.js";
6
+ export function resolveVoiceProvider(registry, kind, settings = {}, overrides = {}) {
7
+ const candidates = entriesOfKind(registry, kind);
8
+ if (candidates.length === 0) return void 0;
9
+ const warnings = [];
10
+ const wanted = (kind === "stt" ? settings.sttProvider : settings.ttsProvider)?.trim();
11
+ const ids = candidates.map(([id]) => id);
12
+ const providerId = selectVoiceEntryId(ids, wanted);
13
+ const entry = candidates[ids.indexOf(providerId)][1];
14
+ if (wanted && wanted !== providerId) {
15
+ warnings.push(
16
+ `settings name '${wanted}' for ${kind}, which is not an enabled ${kind} entry \u2014 using '${providerId}' instead`
17
+ );
18
+ }
19
+ const fallback = wanted !== providerId;
20
+ const settingsOverride = {};
21
+ const override = (kind === "stt" ? settings.sttLanguage : settings.ttsVoice)?.trim();
22
+ if (override) settingsOverride[VOICE_OVERRIDE_PARAM[kind]] = override;
23
+ return {
24
+ providerId,
25
+ entry,
26
+ ...entry.base_url ? { baseUrl: entry.base_url } : {},
27
+ params: { ...entry.params, ...settingsOverride, ...overrides },
28
+ fallback,
29
+ warnings
30
+ };
31
+ }
@@ -2,65 +2,67 @@
2
2
  * Data zones: where a provider entry processes data, and which entries a
3
3
  * host's data-zone rule ("Where may AI process data?") keeps.
4
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.
5
+ * `@skaile/provider-registry` owns the vocabulary (`AI_PROVIDER_DATA_ZONES`,
6
+ * `AI_PROVIDER_DATA_ZONE_RULES`, re-exported by `./provider-registry`) and the
7
+ * catalog's `data_zone` column with `entryDataZone`, `entryAllowedByDataZone`
8
+ * and `filterRegistryByDataZone` in its `/catalog` entry. forge-settings keeps
9
+ * the browser-side view here so the settings panel does not ship the catalog
10
+ * (and its generated model table) to the browser: the same semantics, plus
11
+ * labels, refusal wording and a filter that keeps a merged registry merged.
12
+ * {@link PRESET_DATA_ZONES} is a copy of the catalog's column; a test holds it
13
+ * equal to `@skaile/provider-registry/catalog`.
12
14
  *
13
15
  * Browser-safe on purpose — no `node:` imports.
14
16
  */
15
- import { type AIProviderRegistry, type AIProviderRegistryView, type DataZone, type DataZoneRule, type MergedAIProviderRegistry } from "./provider-registry.js";
17
+ import { type AIProviderDataZone, type AIProviderDataZoneRule, type AIProviderRegistry, type AIProviderRegistryView, type MergedAIProviderRegistry } from "./provider-registry.js";
16
18
  /**
17
19
  * The `data_zone` of each catalog preset, by preset id. Copied from
18
- * `@skaile/workspaces/bridge`'s `AI_PROVIDER_PRESETS` (9.1); change it there
20
+ * `@skaile/provider-registry/catalog`'s `AI_PROVIDER_PRESETS`; change it there
19
21
  * first. A preset this table does not name is `transfer`.
20
22
  */
21
- export declare const PRESET_DATA_ZONES: Readonly<Record<string, DataZone>>;
23
+ export declare const PRESET_DATA_ZONES: Readonly<Record<string, AIProviderDataZone>>;
22
24
  /** What a provider row's zone chip says. */
23
- export declare const DATA_ZONE_LABELS: Readonly<Record<DataZone, string>>;
25
+ export declare const DATA_ZONE_LABELS: Readonly<Record<AIProviderDataZone, string>>;
24
26
  /** What a host calls each rule when it names the rule that keeps an entry out. */
25
- export declare const DATA_ZONE_RULE_LABELS: Readonly<Record<DataZoneRule, string>>;
26
- /** Whether `value` is one of {@link DATA_ZONES}. */
27
- export declare function isDataZone(value: unknown): value is DataZone;
28
- /** Whether `value` is one of {@link DATA_ZONE_RULES}. */
29
- export declare function isDataZoneRule(value: unknown): value is DataZoneRule;
27
+ export declare const DATA_ZONE_RULE_LABELS: Readonly<Record<AIProviderDataZoneRule, string>>;
28
+ /** Whether `value` is one of `AI_PROVIDER_DATA_ZONES`. */
29
+ export declare function isDataZone(value: unknown): value is AIProviderDataZone;
30
+ /** Whether `value` is one of `AI_PROVIDER_DATA_ZONE_RULES`. */
31
+ export declare function isDataZoneRule(value: unknown): value is AIProviderDataZoneRule;
30
32
  /** The zone of catalog preset `presetId`, or `undefined` for an id the catalog does not name. */
31
- export declare function presetDataZone(presetId: string | undefined): DataZone | undefined;
33
+ export declare function presetDataZone(presetId: string | undefined): AIProviderDataZone | undefined;
32
34
  /**
33
35
  * Where an entry processes data: its own `data_zone`, else the zone of the
34
36
  * catalog preset it names, else `transfer` (an unknown endpoint is assumed to
35
37
  * leave the EU).
36
38
  */
37
39
  export declare function entryDataZone(entry: {
38
- data_zone?: DataZone;
40
+ data_zone?: AIProviderDataZone;
39
41
  preset?: string;
40
- }): DataZone;
42
+ }): AIProviderDataZone;
41
43
  /**
42
44
  * Whether `rule` allows an entry in `zone`: `any` allows all; `eu` allows
43
45
  * `eu` and `local`; `eu_options` additionally allows `eu_option` when
44
46
  * `euProcessing` is `true`.
45
47
  */
46
- export declare function dataZoneAllows(rule: DataZoneRule, zone: DataZone, euProcessing: boolean | undefined): boolean;
48
+ export declare function dataZoneAllows(rule: AIProviderDataZoneRule, zone: AIProviderDataZone, euProcessing: boolean | undefined): boolean;
47
49
  /** Whether `rule` allows `entry` ({@link dataZoneAllows} over {@link entryDataZone}). */
48
50
  export declare function entryAllowedByDataZone(entry: {
49
- data_zone?: DataZone;
51
+ data_zone?: AIProviderDataZone;
50
52
  preset?: string;
51
53
  eu_processing?: boolean;
52
- }, rule: DataZoneRule): boolean;
54
+ }, rule: AIProviderDataZoneRule): boolean;
53
55
  /**
54
56
  * Why `rule` keeps an entry in `zone` out, naming the rule, or `undefined`
55
57
  * when the rule allows it. Secret-free, safe to render.
56
58
  */
57
- export declare function dataZoneRefusal(rule: DataZoneRule, zone: DataZone, euProcessing: boolean | undefined): string | undefined;
59
+ export declare function dataZoneRefusal(rule: AIProviderDataZoneRule, zone: AIProviderDataZone, euProcessing: boolean | undefined): string | undefined;
58
60
  /**
59
61
  * The registry with only the entries `rule` allows, every kind alike
60
62
  * (`llm`, `embed`, `stt`, `tts`). Order is kept; the input is not mutated. A
61
63
  * merged registry comes back merged, its `order`, `origin` and `locked`
62
64
  * narrowed to the kept entries.
63
65
  */
64
- export declare function filterRegistryByDataZone(registry: AIProviderRegistry, rule: DataZoneRule): AIProviderRegistry;
65
- export declare function filterRegistryByDataZone(registry: MergedAIProviderRegistry, rule: DataZoneRule): MergedAIProviderRegistry;
66
- export declare function filterRegistryByDataZone(registry: AIProviderRegistryView, rule: DataZoneRule): AIProviderRegistryView;
66
+ export declare function filterRegistryByDataZone(registry: AIProviderRegistry, rule: AIProviderDataZoneRule): AIProviderRegistry;
67
+ export declare function filterRegistryByDataZone(registry: MergedAIProviderRegistry, rule: AIProviderDataZoneRule): MergedAIProviderRegistry;
68
+ export declare function filterRegistryByDataZone(registry: AIProviderRegistryView, rule: AIProviderDataZoneRule): AIProviderRegistryView;
@@ -1,6 +1,6 @@
1
1
  import {
2
- DATA_ZONE_RULES,
3
- DATA_ZONES,
2
+ AI_PROVIDER_DATA_ZONE_RULES,
3
+ AI_PROVIDER_DATA_ZONES,
4
4
  isMergedRegistry
5
5
  } from "./provider-registry.js";
6
6
  export const PRESET_DATA_ZONES = {
@@ -37,10 +37,10 @@ export const DATA_ZONE_RULE_LABELS = {
37
37
  eu: "EU only"
38
38
  };
39
39
  export function isDataZone(value) {
40
- return typeof value === "string" && DATA_ZONES.includes(value);
40
+ return typeof value === "string" && AI_PROVIDER_DATA_ZONES.includes(value);
41
41
  }
42
42
  export function isDataZoneRule(value) {
43
- return typeof value === "string" && DATA_ZONE_RULES.includes(value);
43
+ return typeof value === "string" && AI_PROVIDER_DATA_ZONE_RULES.includes(value);
44
44
  }
45
45
  export function presetDataZone(presetId) {
46
46
  return presetId !== void 0 && Object.hasOwn(PRESET_DATA_ZONES, presetId) ? PRESET_DATA_ZONES[presetId] : void 0;