@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
@@ -1,3 +1,4 @@
1
+ import type { AIProviderDataZoneRule } from "../shared/provider-registry.js";
1
2
  import type { ProvidersPanelClient } from "../vue/providers-client.js";
2
3
  import { type ProvidersPanelLayer } from "../vue/useProvidersPanel.js";
3
4
  type __VLS_Props = {
@@ -22,12 +23,24 @@ type __VLS_Props = {
22
23
  readOnly?: boolean;
23
24
  /** The line above the list. Absent: the default text for `layer`; a string replaces it; `false` hides it. */
24
25
  note?: string | false;
26
+ /**
27
+ * Expert mode: the sign-in step also offers the raw reference form
28
+ * (`env:` / `keyring:` / `cmd:` / `platform:`), and expert-only
29
+ * parameters (`api_version`) become editable.
30
+ */
31
+ expert?: boolean;
32
+ /**
33
+ * The host's data-zone rule: rows outside it show a muted note naming the
34
+ * rule (`provider-blocked-<id>`). Absent or `any`: none.
35
+ */
36
+ dataZoneRule?: AIProviderDataZoneRule;
25
37
  };
26
38
  declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
27
39
  changed: () => any;
28
40
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
29
41
  onChanged?: (() => any) | undefined;
30
42
  }>, {
43
+ expert: boolean;
31
44
  environmentEnabled: boolean;
32
45
  note: string | false;
33
46
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
@@ -4,11 +4,24 @@
4
4
  *
5
5
  * `@skaile/forge-editor`'s AI routes ask the host's `ModelProvider`
6
6
  * (`listModels` / `languageModel`) before their own runtime-config presets.
7
- * This adapter answers from the host's registry: only `llm` entries on the
8
- * `openai-completions` surface are offered, because the editor's transport is a
9
- * chat-completions client (`@ai-sdk/openai-compatible`). A profile's id is the
10
- * registry selector `<providerId>/<modelId>`, which `languageModel` receives
11
- * back as `{ preset: id }`.
7
+ * This adapter answers from the host's registry and is a thin layer over
8
+ * `@skaile/forge-settings/ai-sdk`: a profile's id is the registry selector
9
+ * `<providerId>/<modelId>`, which `languageModel` receives back as
10
+ * `{ preset: id }` and resolves through the same constructor, credential
11
+ * resolver and policy as every other registry model. Every `llm` entry whose
12
+ * surface the AI SDK constructor speaks (`openai-completions`,
13
+ * `openai-responses`, `anthropic-messages`) and that has an endpoint is
14
+ * offered.
15
+ *
16
+ * Listing combines what the registry declares (`model_map` / `capabilities`)
17
+ * with what an entry's own models list reports, for entries that declare
18
+ * `discovery` or declare no models at all. Declared models work without a
19
+ * models list; a declared identity wins over a discovered one.
20
+ *
21
+ * The registry is authoritative for the selectors it owns: an empty registry
22
+ * lists `[]`, and a `<providerId>/…` selector naming a registry entry that is
23
+ * disabled, refused or unresolvable throws instead of falling through to the
24
+ * editor's presets. Only a selector the registry does not own answers `null`.
12
25
  *
13
26
  * The registry and the credential environment are supplied by the host on every
14
27
  * call, so provider edits apply without a restart and nothing reads the ambient
@@ -16,35 +29,61 @@
16
29
  * them to HTTP in `onFailure`.
17
30
  *
18
31
  * `@skaile/forge-editor` is an optional peer of this entry only, for its
19
- * `ModelProvider` contract (types). `@ai-sdk/openai-compatible` builds the model.
32
+ * `ModelProvider` contract (types).
20
33
  */
21
34
  import type { ModelProfile, ModelRef } from "@skaile/forge-editor/ai";
22
35
  import type { ModelProvider } from "@skaile/forge-editor/server";
36
+ import { type LanguageModelObject } from "@skaile/provider-registry/ai-sdk";
23
37
  import { ModelResolutionError } from "../model/errors.js";
24
38
  import { type CredentialPolicy } from "../server/credential-policy.js";
25
39
  import { type AIProviderRegistryView } from "../shared/provider-registry.js";
26
- export { ModelResolutionError } from "../model/errors.js";
27
40
  export type { ModelResolutionCode } from "../model/errors.js";
28
- /** Policy for an editor model call; see `CredentialPolicy`. */
41
+ export { ModelResolutionError } from "../model/errors.js";
42
+ /** Policy and credential store for an editor model call; see `CredentialPolicy`. */
29
43
  export interface EditorModelPolicyOptions {
30
44
  /** Which credential references may be resolved; refused entries are not offered. */
31
45
  policy?: CredentialPolicy;
46
+ /** Local encrypted store for `keyring:` references; else `FORGE_CREDENTIAL_STORE` of the env. */
47
+ path?: string;
48
+ /** 64-char hex key of the local store; else `FORGE_CREDENTIAL_KEY` of the env. */
49
+ key?: string;
32
50
  }
33
51
  /**
34
- * The registry's chat-completions models as editor model profiles, in registry
35
- * order. With a policy, entries it refuses are left out.
52
+ * The models the registry declares (`model_map` / `capabilities`) as editor
53
+ * model profiles, in registry order — no network. With a policy, entries it
54
+ * refuses are left out. {@link createEditorModelProvider} lists these plus the
55
+ * models discovered live.
36
56
  */
37
57
  export declare function editorModelProfiles(registry: AIProviderRegistryView, options?: EditorModelPolicyOptions): ModelProfile[];
58
+ /**
59
+ * Declared and discovered models as editor profiles, in registry order, one
60
+ * profile per selector.
61
+ *
62
+ * An entry's models list is asked when the entry declares `discovery` (and not
63
+ * `type: none`) or declares no models; it is asked with the credential the
64
+ * policy allows, exactly as inference resolves it. A failed or unsupported
65
+ * list leaves that entry with its declared models. A discovered id that a
66
+ * declared model already names (as selector or as mapped provider id) is
67
+ * dropped: the declared identity wins.
68
+ */
69
+ export declare function listEditorModelProfiles(registry: AIProviderRegistryView, env: Record<string, string | undefined>, options?: EditorModelPolicyOptions): Promise<ModelProfile[]>;
38
70
  /**
39
71
  * The language model of selector `ref.preset`, or `null` when the registry does
40
- * not serve it (a browser-defined `{ endpoint, model, apiKey }` reference is the
41
- * editor's own to resolve).
72
+ * not own it: a browser-defined `{ endpoint, model, apiKey }` reference, or a
73
+ * preset id whose leading segment names no registry entry, is the editor's own
74
+ * to resolve.
75
+ *
76
+ * Delegates to `resolveRegistryLanguageModel` (`@skaile/forge-settings/ai-sdk`),
77
+ * so the selector's model id may be declared or discovered: an id the entry's
78
+ * `model_map` maps is sent as the mapped id, any other is sent verbatim.
42
79
  *
43
- * @throws {ModelResolutionError} `policy_refused` when the policy refuses the
44
- * entry (checked before its credential is read); `unresolved` when its
45
- * credential reference does not resolve. Neither message carries a secret.
80
+ * @throws {ModelResolutionError} for an owned selector: `unknown_selector` when
81
+ * the entry is disabled or not an `llm` entry, `policy_refused` when the
82
+ * policy refuses it (checked before its credential is read), `unresolved`
83
+ * when its credential, endpoint or SDK does not resolve. No message carries a
84
+ * secret.
46
85
  */
47
- export declare function editorLanguageModel(registry: AIProviderRegistryView, env: Record<string, string | undefined>, ref: ModelRef, options?: EditorModelPolicyOptions): import("@ai-sdk/provider").LanguageModelV4 | null;
86
+ export declare function editorLanguageModel(registry: AIProviderRegistryView, env: Record<string, string | undefined>, ref: ModelRef, options?: EditorModelPolicyOptions): Promise<LanguageModelObject | null>;
48
87
  /** Options for {@link createEditorModelProvider}. */
49
88
  export interface EditorModelProviderOptions extends EditorModelPolicyOptions {
50
89
  /** The registry to serve, read on every call. */
@@ -1,63 +1,106 @@
1
- import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
2
- import { ModelResolutionError, assertEntryAllowed } from "../model/errors.js";
1
+ import { resolveEntryBaseUrl } from "@skaile/provider-registry";
2
+ import { AI_SDK_SURFACES } from "@skaile/provider-registry/ai-sdk";
3
+ import { resolveRegistryLanguageModel } from "../ai-sdk/index.js";
4
+ import { ModelResolutionError } from "../model/errors.js";
5
+ import { credentialStoreOptions, selectorProviderId } from "../model/selector.js";
3
6
  import { entryPolicyRefusal } from "../server/credential-policy.js";
4
- import { resolveEntryCredential } from "../server/credential-store.js";
7
+ import { discoverProviderModels } from "../server/provider-discovery.js";
5
8
  import { listAIProviderModels } from "../server/provider-service.js";
6
9
  import {
10
+ asMergedRegistry,
7
11
  entriesOfKind
8
12
  } from "../shared/provider-registry.js";
9
13
  export { ModelResolutionError } from "../model/errors.js";
10
- const EDITOR_SURFACE = "openai-completions";
11
- function editorEntries(registry) {
14
+ function editorEntries(registry, policy) {
12
15
  const entries = /* @__PURE__ */ new Map();
13
16
  for (const [id, entry] of entriesOfKind(registry, "llm")) {
14
- if (entry.surface === EDITOR_SURFACE && entry.base_url) entries.set(id, entry);
17
+ const surface = entry.surface ?? "openai-completions";
18
+ if (!AI_SDK_SURFACES.includes(surface)) continue;
19
+ if (!resolveEntryBaseUrl(entry)) continue;
20
+ if (entryPolicyRefusal(entry, policy)) continue;
21
+ entries.set(id, entry);
15
22
  }
16
23
  return entries;
17
24
  }
18
- export function editorModelProfiles(registry, options = {}) {
19
- const entries = editorEntries(registry);
20
- const allowed = (providerId) => {
21
- const entry = entries.get(providerId);
22
- return entry !== void 0 && !entryPolicyRefusal(entry, options.policy);
23
- };
24
- return listAIProviderModels(registry).filter((model) => allowed(model.providerId)).map((model) => ({
25
+ function registryProfile(providerId, entry, model) {
26
+ return {
25
27
  id: model.id,
26
- label: `${model.providerId} \xB7 ${model.modelId}`,
27
- endpoint: entries.get(model.providerId).base_url,
28
+ label: `${providerId} \xB7 ${model.name}`,
29
+ endpoint: resolveEntryBaseUrl(entry),
28
30
  model: model.providerModelId,
29
31
  source: "preset"
30
- }));
32
+ };
33
+ }
34
+ function declaredProfiles(registry, entries) {
35
+ const byProvider = /* @__PURE__ */ new Map();
36
+ for (const model of listAIProviderModels(registry)) {
37
+ const entry = entries.get(model.providerId);
38
+ if (!entry) continue;
39
+ const profile = registryProfile(model.providerId, entry, {
40
+ id: model.id,
41
+ name: model.modelId,
42
+ providerModelId: model.providerModelId
43
+ });
44
+ const own = byProvider.get(model.providerId);
45
+ if (own) own.push(profile);
46
+ else byProvider.set(model.providerId, [profile]);
47
+ }
48
+ return byProvider;
49
+ }
50
+ export function editorModelProfiles(registry, options = {}) {
51
+ return [...declaredProfiles(registry, editorEntries(registry, options.policy)).values()].flat();
31
52
  }
32
- export function editorLanguageModel(registry, env, ref, options = {}) {
53
+ export async function listEditorModelProfiles(registry, env, options = {}) {
54
+ const entries = editorEntries(registry, options.policy);
55
+ const declared = declaredProfiles(registry, entries);
56
+ const listed = await Promise.all(
57
+ [...entries].map(async ([providerId, entry]) => {
58
+ const own = declared.get(providerId) ?? [];
59
+ const listable = entry.discovery !== void 0 ? entry.discovery.type !== "none" : own.length === 0;
60
+ if (!listable) return own;
61
+ const result = await discoverProviderModels(
62
+ providerId,
63
+ registry,
64
+ credentialStoreOptions(env, options)
65
+ );
66
+ if (result.status !== "live") return own;
67
+ const taken = new Set(
68
+ own.flatMap((profile) => [profile.id, `${providerId}/${profile.model}`])
69
+ );
70
+ const discovered = result.models.flatMap((model) => {
71
+ const id = `${providerId}/${model.id}`;
72
+ if (taken.has(id)) return [];
73
+ taken.add(id);
74
+ return [
75
+ registryProfile(providerId, entry, { id, name: model.name, providerModelId: model.id })
76
+ ];
77
+ });
78
+ return [...own, ...discovered];
79
+ })
80
+ );
81
+ return listed.flat();
82
+ }
83
+ export async function editorLanguageModel(registry, env, ref, options = {}) {
33
84
  if (!("preset" in ref)) return null;
34
- const model = listAIProviderModels(registry).find((candidate) => candidate.id === ref.preset);
35
- const entry = model && editorEntries(registry).get(model.providerId);
36
- if (!model || !entry) return null;
37
- assertEntryAllowed(entry, model.providerId, options.policy);
38
- const credential = resolveEntryCredential(entry, {
39
- env,
40
- ...options.policy ? { policy: options.policy } : {}
85
+ const merged = asMergedRegistry(registry);
86
+ const head = selectorProviderId(ref.preset);
87
+ if (head === void 0 || merged.providers[head] === void 0) return null;
88
+ const resolved = await resolveRegistryLanguageModel(ref.preset, {
89
+ registry: merged,
90
+ ...credentialStoreOptions(env, options)
41
91
  });
42
- if (credential.error && !credential.value) {
43
- throw new ModelResolutionError(
44
- "unresolved",
45
- `provider '${model.providerId}': ${credential.error}`,
46
- { providerId: model.providerId }
47
- );
48
- }
49
- return createOpenAICompatible({
50
- name: model.providerId,
51
- baseURL: entry.base_url,
52
- ...credential.value ? { apiKey: credential.value } : {},
53
- ...entry.headers ? { headers: entry.headers } : {}
54
- }).chatModel(model.providerModelId);
92
+ return resolved.model;
55
93
  }
56
94
  export function createEditorModelProvider(options) {
57
95
  return {
58
96
  async languageModel(ref) {
59
97
  try {
60
- return editorLanguageModel(await options.registry(), await options.env(), ref, options);
98
+ return await editorLanguageModel(
99
+ await options.registry(),
100
+ await options.env(),
101
+ ref,
102
+ options
103
+ );
61
104
  } catch (error) {
62
105
  if (error instanceof ModelResolutionError && options.onFailure)
63
106
  throw options.onFailure(error);
@@ -65,8 +108,7 @@ export function createEditorModelProvider(options) {
65
108
  }
66
109
  },
67
110
  async listModels() {
68
- const profiles = editorModelProfiles(await options.registry(), options);
69
- return profiles.length > 0 ? profiles : null;
111
+ return listEditorModelProfiles(await options.registry(), await options.env(), options);
70
112
  }
71
113
  };
72
114
  }
@@ -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,24 +1,25 @@
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
  */
11
+ export * from "../shared/data-zone.js";
9
12
  export * from "../shared/harness-providers.js";
10
13
  export * from "../shared/harnesses.js";
11
14
  export * from "../shared/model-roles.js";
15
+ export * from "../shared/provider-accounts.js";
12
16
  export * from "../shared/provider-registry.js";
13
- export type { DomainConfig, SkaileSettings } from "../shared/settings-types.js";
14
17
  export * from "./credential-policy.js";
15
18
  export * from "./credential-store.js";
16
19
  export * from "./env.js";
17
20
  export * from "./env-layers.js";
18
- export * from "./harness-settings.js";
19
21
  export * from "./provider-discovery.js";
20
22
  export * from "./provider-layers.js";
21
23
  export * from "./provider-service.js";
22
- export * from "./provider-settings.js";
23
24
  export * from "./provider-write.js";
24
- export * from "./settings-defaults.js";
25
+ export * from "./voice-provider.js";
@@ -1,15 +1,15 @@
1
+ export * from "../shared/data-zone.js";
1
2
  export * from "../shared/harness-providers.js";
2
3
  export * from "../shared/harnesses.js";
3
4
  export * from "../shared/model-roles.js";
5
+ export * from "../shared/provider-accounts.js";
4
6
  export * from "../shared/provider-registry.js";
5
7
  export * from "./credential-policy.js";
6
8
  export * from "./credential-store.js";
7
9
  export * from "./env.js";
8
10
  export * from "./env-layers.js";
9
- export * from "./harness-settings.js";
10
11
  export * from "./provider-discovery.js";
11
12
  export * from "./provider-layers.js";
12
13
  export * from "./provider-service.js";
13
- export * from "./provider-settings.js";
14
14
  export * from "./provider-write.js";
15
- 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,