@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
package/CHANGELOG.md CHANGED
@@ -1,5 +1,45 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.0.0
4
+
5
+ ### Breaking
6
+
7
+ - `@skaile/workspaces` dependency → optional peer, needed only by the new `@skaile/forge-settings/workspaces` entry. `/server`, `/vue`, `/nuxt`, `/ai-sdk` and `/editor` no longer import it at runtime or in their declarations; the registry schema, catalog, discovery and model constructor come from the new dependency `@skaile/provider-registry`.
8
+ - `SkaileSettings`, `DomainConfig`, `SETTINGS_DEFAULTS`, `migrateLegacyHarnessSettings`, `migrateLegacyProviderSettings`, `LegacySettingsMigration` and the harness settings rules (`HarnessSettings`, `LockFlags`, `LockFlagStatus`, `resolveLockFlags`, `enabledHarnessIds`, `HarnessRow`, `buildHarnessRows`, `HarnessSettingsPatch`, `validateHarnessSettingsPatch`, `HarnessLayer`, `applyHarnessLayers`) from `@skaile/forge-settings/server` → `@skaile/forge-settings/workspaces`. Voice selection (`resolveVoiceProvider`, `VoiceSettings`, `VoiceProviderSelection`) stays in `/server`; `VoiceSettings` is now its own interface with the same four fields instead of `Pick<SkaileSettings, …>`, so a `SkaileSettings` still passes.
9
+ - `resolveRegistryLanguageModel(selector, { projectDir, registry?, env, roles?, policy? })` → `resolveRegistryLanguageModel(selector, { registry, env, roles?, policy?, path?, key? })`: `registry` is required and nothing is read from disk. Project-oriented callers → `resolveWorkspaceLanguageModel(selector, { projectDir, registry?, env, roles?, policy?, path?, key? })` from `/workspaces`, which loads the project's registry once and keeps the keyring at `<skaile home>/forge-credentials.json`.
10
+ - Credential store: implicit `<skaile home>/forge-credentials.json` → explicit `options.path`, else `FORGE_CREDENTIAL_STORE` of the environment; with neither, a keyring operation throws `FORGE_CREDENTIAL_STORE or credential store path is required` (resolution reports it as the credential error). Workspaces hosts pass `workspaceCredentialStorePath(env)` (or pin it into `FORGE_CREDENTIAL_STORE`) to keep their file. Env-only and keyless providers need no store.
11
+ - `editorLanguageModel(registry, env, ref, options)` synchronous `LanguageModel | null` → `Promise<LanguageModel | null>`, built by the shared `@skaile/provider-registry/ai-sdk` constructor (adds `openai-responses` and `anthropic-messages` entries; the `<providerId>/<modelId>` profile selector is unchanged). A `<providerId>/…` selector whose provider is a registry entry now throws when disabled or refused instead of answering `null`.
12
+ - `createEditorModelProvider(…).listModels()` returns `[]` for a registry with no servable models (was `null`, which fell through to the editor's presets) and lists discovered models next to the declared ones.
13
+ - `shared/provider-registry` mirror types → re-exports of the canonical `@skaile/provider-registry` types (same names); `parseCredentialRef`, `formatCredentialRef` and `isEntryEnabled` are the canonical functions.
14
+ - 1.4.0 harness additions move with the harness rules to `/workspaces`: `HarnessRowProviders`, `buildHarnessRows(harnesses, settings, detections?, { candidates, dataZoneRule }?)` with `HarnessRow.roles` / `roleAssignments` / `blockedProviders`, `validateHarnessSettingsPatch`'s `harnessRoles` (and `ctx.runnableModels`), `applyHarnessLayers`' `roles`, and the `SkaileSettings.harnessRoles` field: `import { buildHarnessRows, type HarnessRowProviders, type SkaileSettings } from '@skaile/forge-settings/server'` → `… from '@skaile/forge-settings/workspaces'`. `HARNESS_ROLES`, `harnessRoleIds` and the browser-safe harness-provider rule (`resolveHarnessProviders`, `harnessProviderBlock`, `HarnessProviderBlock`, `HarnessProviderCandidate`, `HarnessProviderOptions`) stay in `/server` and `/vue`.
15
+ - 1.4.0 data-zone vocabulary mirror → the registry's own names, re-exported by `/server` and `/vue` from `@skaile/provider-registry` (values unchanged): `DATA_ZONES` → `AI_PROVIDER_DATA_ZONES`, `DATA_ZONE_RULES` → `AI_PROVIDER_DATA_ZONE_RULES`, type `DataZone` → `AIProviderDataZone`, type `DataZoneRule` → `AIProviderDataZoneRule`. Every signature that took or returned them (`entryDataZone`, `entryAllowedByDataZone`, `dataZoneAllows`, `dataZoneRefusal`, `filterRegistryByDataZone`, `presetDataZone`, `ProviderInfo.dataZone`, `HarnessProviderBlock.rule` / `.zone`, the `dataZoneRule` panel prop) now names the canonical types. The browser-safe helpers `PRESET_DATA_ZONES`, `DATA_ZONE_LABELS`, `DATA_ZONE_RULE_LABELS`, `isDataZone`, `isDataZoneRule`, `presetDataZone`, `entryDataZone`, `entryAllowedByDataZone`, `dataZoneAllows`, `dataZoneRefusal` and `filterRegistryByDataZone` (which keeps a merged registry merged) stay in `/server` and `/vue`; `PRESET_DATA_ZONES` is tested equal to `@skaile/provider-registry/catalog`.
16
+ - 1.4.0 `isModelAllowed` mirror → the canonical `isModelAllowed` of `@skaile/provider-registry`, still re-exported by `/server` and `/vue` under the same name and with the same rules; its entry parameter is `AIProviderModelAllowListFields` (`allowed_models?: string[]`), so pass a mutable array instead of a `readonly string[]`. New re-exports next to it: `allowedModelIds`, `AIProviderModelAllowListFields`.
17
+ - 1.4.0 entry fields `data_zone`, `eu_processing` and `allowed_models` are validated by `@skaile/provider-registry` (was: the `@skaile/workspaces` 9.1 schema), so `upsertProviderEntry` and the generic entries accept them with any peer or none. `/workspaces`' `loadWorkspaceProviderRegistry` / `resolveWorkspaceLanguageModel` load the project registry through the installed `@skaile/workspaces`, whose schema before 9.1 refuses those keys: a registry that uses them needs the peer at 9.1 or later.
18
+
19
+ ### Added
20
+
21
+ - `discoverProviderModels` asks any entry that speaks a models list (`discovery: { type: 'openai-models-list' | 'anthropic-models-list' }` or an OpenAI/Anthropic surface) at its own `base_url`, through `fetchEntryModels`: no catalog preset needed, `credential.kind: 'none'` queries without `Authorization`, static headers and the credential carrier match inference. Options `path`, `key`, `signal` (`signal` also settles a catalog-preset listing as cancelled).
22
+ - `CREDENTIAL_STORE_PATH_REQUIRED` (`/server`): the message a keyring operation without a store path throws.
23
+ - `createEditorModelProvider` / `editorLanguageModel` options `path` and `key` (local store); `listEditorModelProfiles(registry, env, options)`.
24
+ - `asMergedRegistry(view)` (browser-safe).
25
+ - `/workspaces`: `resolveWorkspaceLanguageModel`, `loadWorkspaceProviderRegistry(projectDir)`, `workspaceCredentialStorePath(env?)` (`FORGE_CREDENTIAL_STORE` of `env`, else `<skaile home>/forge-credentials.json`), `readWorkspaceModelEndpoint({ env, projectDir? })` (`SKAILE_MODEL_ENDPOINT_URL` + `SKAILE_MODEL_ENDPOINT_TOKEN`, else `<projectDir>/.skaile/model-endpoint.json`; `null` when absent, token-free configuration errors) and the env names `WORKSPACE_MODEL_ENDPOINT_URL_ENV` / `WORKSPACE_MODEL_ENDPOINT_TOKEN_ENV`.
26
+ - Accepts `@skaile/workspaces` ^7.8.0 through ^10.0.0 as the optional `/workspaces` peer (`^7.8.0 || ^8.0.0 || ^9.0.0 || ^10.0.0`; 1.4.0 required `^9.1.0` as a dependency). The `/workspaces` entry imports no 9.1 symbol: its imports (`loadMergedProviderRegistry`, `globalSkaileDir`, `SETTINGS_DEFAULTS`, `SkaileSettings`, the `HarnessInfo` / `HarnessDetection` types) exist in 7.8.0 through 10.0.0, and the entry typechecks against each of 7.8.0, 8.0.0, 9.0.0 and 10.0.0. 1.4.0's `^9.1.0` was for the registry schema of `data_zone` / `eu_processing` / `allowed_models`, which the generic entries now take from `@skaile/provider-registry`; only a project registry that uses those keys needs the peer at 9.1 or later (see Breaking). 10.0.0 moved the registry primitives to `@skaile/provider-registry`, which this package now takes from there.
27
+
28
+ ## 1.4.0
29
+
30
+ - `<ForgeProvidersPanel>` adds a provider as a flow: a library of the catalog presets (plus Custom), then *How do you sign in?* listing only the kinds the preset accepts, the fields of that kind and the preset's routing parameters, then a done step that tests every created entry when a `client` is passed. New boolean prop `expert` (default `false`): adds the raw Reference form (`env:` / `keyring:` / `cmd:` / `platform:`) to the sign-in step and makes `api_version` editable. Detected-preset Add and Edit keep working; Edit skips the library.
31
+ - **Microsoft Azure** library card: resource name, one sign-in (Entra ID or API key), then Azure OpenAI (deployment rows → `model_map`, `api_version` prefilled `2025-04-01-preview`) and/or Claude on Foundry. Writes up to two entries (presets `azure-openai`, `foundry`) sharing `params.resource` and the credential; a refusal after the first write names the entries already created.
32
+ - Provider rows show an account line ("Microsoft Azure · Entra ID · contoso-ai": family · sign-in kind · resource) and, when the entry view carries `credentialExpiresAt`, "Expires in N days · Renew" (Renew re-opens the sign-in). No registry API reports an expiry yet, so the line appears only for hosts that set the field.
33
+ - New test ids: `provider-library`, `provider-library-<presetId>`, `provider-library-azure`, `provider-library-custom`, `provider-back`, `provider-signin`, `provider-signin-<optionId>`, `provider-param-<key>`, `provider-azure-resource`, `provider-azure-openai`, `provider-azure-foundry`, `provider-azure-deployments`, `provider-azure-deployment-<i>`, `provider-azure-model-<i>`, `provider-azure-deployment-remove-<i>`, `provider-azure-deployment-add`, `provider-azure-api-version`, `provider-done`, `provider-created-<id>`, `provider-created-result-<id>`, `provider-created-test-<id>`, `provider-done-close`, `provider-account-<id>`, `provider-expiry-<id>`, `provider-renew-<id>`. Existing ids are unchanged.
34
+ - `/server` and `/vue` export the browser-safe account helpers: `presetSignInOptions`, `signInCredential`, `signInOptionOf`, `missingRequiredParams`, `presetParamKeys`, `defaultParamValue`, `isAzureTransport`, `providerAccountLine`, `providerFamilyLabel`, `credentialExpiry`, `deploymentModelMap`, `azureAccountEntries`, `CREDENTIAL_KIND_LABELS`, `PROVIDER_PARAM_LABELS`, `EXPERT_PARAMS`, `AZURE_TRANSPORTS`, `AZURE_FAMILY_LABEL`, `AZURE_OPENAI_PRESET`, `FOUNDRY_PRESET`, `AZURE_OPENAI_DEFAULT_API_VERSION`, and their types.
35
+ - `upsertProviderEntry` accepts `params` (an omitting body keeps them, `params: {}` removes them). A catalog preset's required parameter that neither the entry nor a lower document's entry supplies is refused with 400 naming it (`data.missingParams`); keys the transport does not accept are refused by the SDK schema as before (`data.diagnostics`). `withProviderLayers` carries `params` in `layerFields`.
36
+ - `providerPresetSummaries()` adds each preset's `transport`, `params` and `required_params`; `listAIProviders` views add `transport` and `transportParams` for `llm` / `embed` entries.
37
+ - Harness roles ("models per task"): new setting `harnessRoles` (harness id → role → `providerId/modelId`), exported `HARNESS_ROLES` (omp: default, smol, slow, vision, plan, designer, commit, tiny, task, advisor; continue: chat, edit, apply, autocomplete, embed, rerank, summarize) and `harnessRoleIds(harnessId)`. `validateHarnessSettingsPatch` validates and patches it per role (optional `ctx.runnableModels` refuses selectors that cannot run; empty/`null` removes a role, `{}`/`null` a harness's roles); `applyHarnessLayers` returns `roles`, layered per role and frozen by `lockModel`; `HarnessRow` carries `roles` and `roleAssignments`.
38
+ - Data zones (mirror of `@skaile/workspaces` 9.1, browser-safe, exported from `/server` and `/vue`): `DATA_ZONES` (`eu`, `eu_option`, `transfer`, `local`), `DATA_ZONE_RULES` (`any`, `eu_options`, `eu`), types `DataZone` / `DataZoneRule`, `PRESET_DATA_ZONES` (each catalog preset's zone, copied from the SDK catalog), `DATA_ZONE_LABELS` (EU / EU option / Transfer / Local), `DATA_ZONE_RULE_LABELS`, `isDataZone`, `isDataZoneRule`, `presetDataZone`, `entryDataZone` (the entry's `data_zone`, else the preset's, else `transfer`), `dataZoneAllows`, `entryAllowedByDataZone`, `dataZoneRefusal` and `filterRegistryByDataZone` (`any` keeps all; `eu_options` keeps `eu`, `local` and `eu_option` with `eu_processing: true`; `eu` keeps `eu` and `local`; a merged registry keeps its shape). The registry mirror gains `data_zone`, `eu_processing` and `allowed_models`, plus `isModelAllowed(entry, modelId)`.
39
+ - `resolveHarnessProviders(harness, candidates, selection, { dataZoneRule })`: an otherwise-eligible entry outside the rule is listed in `blocked` (reason `blocked-by-rule`, the rule, its label, the entry's zone and a message naming the rule). It is never eligible, never auto-chosen and never counted towards `needs-choice`; a stored pick of it is reported in `stale`. `HarnessProviderCandidate` reads `preset`, `dataZone` and `euProcessing`; `harnessProviderBlock(harness, candidate, rule)` answers for one entry. `buildHarnessRows(harnesses, settings, detections?, { candidates, dataZoneRule }?)` fills `HarnessRow.blockedProviders` (entry id → block), so a host table can say why a box cannot be ticked.
40
+ - `upsertProviderEntry` accepts `data_zone` (one of the four zones), `eu_processing` (boolean) and `allowed_models` (a list of distinct, non-empty model ids, trimmed; `[]` allows none); any other value is a 400 naming the field. Like `params`, a body that omits them keeps the stored value; `null` removes it (`allowed_models: null` = every model). `listAIProviders` views carry `dataZone` (effective), `euProcessing`, `allowedModels` (when set) and `modelIds` (the declared `model_map` / `capabilities` ids); `listAIProviderModels` leaves out models outside `allowed_models`. Writing these fields needs `@skaile/workspaces` 9.1 at runtime: 9.0's registry schema refuses them as unknown keys.
41
+ - `<ForgeProvidersPanel>`: each row shows its zone chip and, for `llm` / `embed` entries, its model chip ("All models" / "N models"). An own `eu_option` row has the switch "EU processing is on for this account" (writes `eu_processing`). New optional prop `dataZoneRule`: rows outside it show a muted note naming the rule. A writable own `llm` / `embed` row has a "Models allowed here" section: All models | Only these, the latter a searchable checklist of the entry's declared models or, when it declares none, of `client.models(id)` (the existing listing; when that fails or lists nothing, the reason is shown); Save writes `allowed_models`. New exports `providerModelsChip`, `providerModelChoices`, types `ProvidersPanelModelsMode`, `ProvidersPanelModelsForm`. New test ids: `provider-zone-<id>`, `provider-eu-processing-<id>`, `provider-blocked-<id>`, `provider-models-chip-<id>`, `provider-models-<id>`, `provider-models-mode-<id>`, `provider-models-search-<id>`, `provider-model-<id>-<modelId>`, `provider-models-message-<id>`, `provider-models-error-<id>`, `provider-models-cancel-<id>`, `provider-models-save-<id>`.
42
+
3
43
  ## 1.3.3
4
44
 
5
45
  - Accepts `@skaile/workspaces` ^9.0.0. The only 9.0.0 break is the new `skaile replica` command, which this package does not use; the provider registry, catalog and model-discovery APIs it uses are unchanged.
package/README.md CHANGED
@@ -4,13 +4,14 @@ Provider and settings operations for Skaile forge apps, without the app's databa
4
4
 
5
5
  | Entry | Contents | Peers it needs |
6
6
  |---|---|---|
7
- | `/server` | registry layers and writes, credential store, `.env` layers, provider inventory and discovery, harness settings rules, credential policy | none |
7
+ | `/server` | registry layers and writes, credential store, `.env` layers, provider inventory and discovery, credential policy, model roles, voice selection (`resolveVoiceProvider`) | none |
8
8
  | `/vue` | `useProvidersPanel`, `urlProvidersClient`, `SettingsModalScope`, model-role helpers, browser-safe registry/env/harness vocabulary | `vue` |
9
9
  | `/nuxt` | Nuxt module registering `<ForgeProvidersPanel>` and `<ForgeModelRolesPanel>` | `nuxt`, `@nuxt/ui` |
10
10
  | `/ai-sdk` | AI SDK `LanguageModel` of a registry selector | `ai` + the `@ai-sdk/*` provider the surface needs |
11
- | `/editor` | the editor's `ModelProvider` over a registry | `@skaile/forge-editor`, `@ai-sdk/openai-compatible` |
11
+ | `/editor` | the editor's `ModelProvider` over a registry | `@skaile/forge-editor`, `ai`, the `@ai-sdk/*` provider the surface needs |
12
+ | `/workspaces` | Skaile Workspaces integration: `SkaileSettings`, `SETTINGS_DEFAULTS`, legacy settings migrations, harness settings rules, `workspaceCredentialStorePath`, `resolveWorkspaceLanguageModel`, `readWorkspaceModelEndpoint` | `@skaile/workspaces` |
12
13
 
13
- Importing `/server` loads no Vue, editor, AI SDK or agent session code. Every peer is optional and used by one entry only.
14
+ The registry schema, catalog, discovery and model constructor come from `@skaile/provider-registry` (a dependency). Importing `/server` loads no Vue, editor, AI SDK, agent session code or Skaile Workspaces. Every peer is optional and used by one entry only: `/server`, `/vue`, `/nuxt`, `/ai-sdk` and `/editor` install, load and typecheck without `@skaile/workspaces`; only `/workspaces` needs it.
14
15
 
15
16
  ## What stays with the host
16
17
 
@@ -34,16 +35,20 @@ The policy is checked against `credential.ref` and every `${NAME}` token in `bas
34
35
  ```ts
35
36
  const scope: ProviderWriteScope = { name: 'instance', file, below: [], env, lock, policy } // refuses writes naming a refused ref
36
37
  discoverProviderModels('main', registry, { env, policy })
37
- resolveRegistryLanguageModel('main/gpt-x', { projectDir, env, policy })
38
+ resolveRegistryLanguageModel('main/gpt-x', { registry, env, policy })
38
39
  ```
39
40
 
40
41
  ## Credential store
41
42
 
42
43
  `keyring:<service>/<account>` references are stored AES-256-GCM encrypted in a JSON file; the reference is the additional authenticated data, so a blob cannot be replayed under another reference. The key is `FORGE_CREDENTIAL_KEY` (64 hex characters) or `options.key`. Without it only operations that need the store fail, with `CredentialKeyError` (`statusCode: 503`); `env:` references keep working. A blob that does not authenticate raises `CredentialDecryptError`.
43
44
 
45
+ There is no implicit store location. The file is `options.path`, else `FORGE_CREDENTIAL_STORE` of the environment the call is handed; with neither, a keyring operation throws `FORGE_CREDENTIAL_STORE or credential store path is required` (resolution reports it as the credential's error and sends nothing). Env-only and keyless providers need no store. A Skaile Workspaces host keeps its existing file by passing `workspaceCredentialStorePath()` from `/workspaces` (`<skaile home>/forge-credentials.json`) as `path`, or pinning it into `FORGE_CREDENTIAL_STORE`.
46
+
44
47
  ## Model discovery
45
48
 
46
- `discoverProviderModels(id, registry, { env, policy?, redact? })` returns `live`, `static` (the offline catalog, with the scrubbed reason the live request failed), `unsupported` (no catalog preset: let the admin type a model id) or `error`. Error text never contains a remote response body, and every secret the call resolved (plus `redact`) is removed from it, also in URL-encoded and base64 form.
49
+ `discoverProviderModels(id, registry, { env, policy?, redact?, path?, key?, signal? })` returns `live` (possibly empty), `static` (the entry's own preset catalog, with the scrubbed reason the live request failed — a labelled fallback, never an inference destination), `unsupported` (the entry speaks no models list and has no catalog: let the admin type a model id) or `error`. Error text never contains a remote response body, and every secret the call resolved (plus `redact`) is removed from it, also in URL-encoded and base64 form.
50
+
51
+ Any entry with `discovery: { type: 'openai-models-list' }` (or an OpenAI/Anthropic surface) is asked at its own `base_url`, with its static `headers` and its credential exactly as inference sends them — no catalog preset needed. `credential.kind: 'none'` queries without `Authorization`, so a local model server works keyless. The credential is resolved under the policy first: a refused or unresolvable reference sends no request.
47
52
 
48
53
  ## Models for the editor and the AI SDK
49
54
 
@@ -55,18 +60,36 @@ export const models = createEditorModelProvider({
55
60
  registry: () => getProviderRegistry(home()),
56
61
  env: () => effectiveEnv(),
57
62
  policy, // refused entries are neither listed nor resolved
63
+ path: credentialStoreFile, // keyring: references; else FORGE_CREDENTIAL_STORE of env()
58
64
  onFailure: (e) => createError({ statusCode: e.code === 'policy_refused' ? 403 : 400, statusMessage: e.message }),
59
65
  })
60
66
  ```
61
67
 
68
+ `listModels` answers the declared models (`model_map` / `capabilities`) plus the models each entry's models list reports, one profile per selector (a declared identity wins over a discovered duplicate). Declared models work without a models list. The registry is authoritative: an empty one lists `[]`, and a selector whose provider id is a registry entry either resolves or throws (disabled, refused, unresolvable) — only a selector the registry does not own (or a browser `{ endpoint, model, apiKey }` reference) answers `null` and falls through to the editor's presets. `editorModelProfiles(registry, { policy? })` is the declared-only, network-free list; `await editorLanguageModel(registry, env, ref, options)` is the adapter's model lookup.
69
+
62
70
  ```ts
63
71
  import { resolveRegistryLanguageModel } from '@skaile/forge-settings/ai-sdk'
64
- const { model } = await resolveRegistryLanguageModel('assistant-default/some-model', { projectDir, env, policy })
72
+ const { model } = await resolveRegistryLanguageModel('assistant-default/some-model', { registry, env, policy, path })
65
73
  ```
66
74
 
67
- Role-to-selector mapping belongs to the host's settings document; there is no global model locator. Failures are `ModelResolutionError` with a `code`; messages never carry a secret.
75
+ Resolves against the one `registry` snapshot passed (no disk read), with the same policy-checked credential resolver discovery uses, through `@skaile/provider-registry/ai-sdk` (`openai-completions`, `openai-responses`, `anthropic-messages`). A disabled or missing named provider and an unassigned `@role` fail; nothing is rerouted. Role-to-selector mapping belongs to the host's settings document; there is no global model locator. Failures are `ModelResolutionError` with a `code`; messages never carry a secret.
76
+
77
+ ## Skaile Workspaces
78
+
79
+ ```ts
80
+ import { readWorkspaceModelEndpoint, resolveWorkspaceLanguageModel } from '@skaile/forge-settings/workspaces'
81
+
82
+ // The project's merged registry (user + .skaile/providers.yml + catalog), read once;
83
+ // keyring: references read <skaile home>/forge-credentials.json unless path / FORGE_CREDENTIAL_STORE say otherwise.
84
+ const { model } = await resolveWorkspaceLanguageModel('@assistant', { projectDir, env, roles, policy })
85
+ // Or an already merged (e.g. transform-proxy-routed) registry: { projectDir, registry, env }.
86
+
87
+ // A running `skaile serve --model-endpoint`: SKAILE_MODEL_ENDPOINT_URL + _TOKEN from env (both, http(s)),
88
+ // else <projectDir>/.skaile/model-endpoint.json when projectDir is given; null when neither exists.
89
+ const endpoint = await readWorkspaceModelEndpoint({ env, projectDir }) // { url, token } | null
90
+ ```
68
91
 
69
- Pass `registry` (an already merged registry, e.g. one routed through the Workspaces transform proxy) to resolve against it instead of the providers on disk; the policy check runs on it and Workspaces receives it unchanged.
92
+ `readWorkspaceModelEndpoint` never climbs directories or reads the working directory, and its configuration errors (half an env pair, malformed or incomplete file) never contain the token. `workspaceCredentialStorePath(env?)` is the Workspaces keyring: `FORGE_CREDENTIAL_STORE` of `env`, else `<skaile home>/forge-credentials.json`. The settings document (`SkaileSettings`, `SETTINGS_DEFAULTS`, `migrateLegacyHarnessSettings`, `migrateLegacyProviderSettings`) and harness rules (`buildHarnessRows`, `validateHarnessSettingsPatch`, `applyHarnessLayers`, …) live here too; voice selection (`resolveVoiceProvider`) is generic and lives in `/server` — a `SkaileSettings` passes as its `VoiceSettings`.
70
93
 
71
94
  ## Providers panel
72
95
 
@@ -88,6 +111,26 @@ Without URLs, pass `:client` (a `ProvidersPanelClient`: `list`, `detected`, `pre
88
111
 
89
112
  `note` sets the line above the list: absent keeps the panel's text for `layer`, a string replaces it, `false` hides it (the Add provider button stays, right-aligned).
90
113
 
114
+ **Adding a provider** is a flow: a library of the catalog presets, then *How do you sign in?* with only the kinds the preset accepts (`presetSignInOptions` in `/server` and `/vue`: API key; Entra ID first for the Azure transports; AWS / Google credentials for Bedrock / Vertex; No sign-in for local servers), then the fields of that kind and the preset's routing parameters (`params`: `resource`, `deployment`, `api_version`, …), then the entry is written and, with a client, tested on a done step. `:expert="true"` adds the raw *Reference* form (`env:` / `keyring:` / `cmd:` / `platform:`) to every sign-in step and makes expert-only parameters (`api_version`) editable; the host owns the switch. The **Microsoft Azure** card takes a resource name, one sign-in, and Azure OpenAI (deployment rows → `model_map`) and/or Claude on Foundry; it writes up to two entries (`azure-openai`, `foundry` presets, `azureAccountEntries`) sharing `params.resource` and the credential. Each row shows its account line (`providerAccountLine`: "Microsoft Azure · Entra ID · contoso-ai") and, when the entry view carries `credentialExpiresAt`, "Expires in N days · Renew".
115
+
116
+ `upsertProviderEntry` accepts `params` (kept when a body omits it, removed by `params: {}`). A catalog preset's `required_params` missing from the entry and from every lower document's entry of the same id is refused with 400, the message naming `params.<key>` and `data.missingParams` listing them; keys a transport does not accept are refused by the SDK schema (`data.diagnostics`). `providerPresetSummaries()` carries each preset's `transport`, `params` and `required_params`; provider views carry an `llm`/`embed` entry's `transport` and `transportParams`.
117
+
118
+ ### Data zones and model filters
119
+
120
+ Three entry fields of the `@skaile/provider-registry` schema are writable through `upsertProviderEntry` (a Workspaces runtime reading the same file needs `@skaile/workspaces` 9.1 or later; 9.0's schema refuses them). A body that omits one keeps the stored value, and `null` removes it:
121
+
122
+ |Field|Accepts|Removed by `null` means|
123
+ |---|---|---|
124
+ |`data_zone`|`eu`, `eu_option`, `transfer`, `local` (overrides the preset's zone)|the preset's zone, else `transfer`|
125
+ |`eu_processing`|boolean: the account's EU-processing setting is on (only an `eu_option` entry reads it)|off|
126
+ |`allowed_models`|distinct, non-empty model ids (`model_map` keys, or provider ids for a discovery entry); `[]` allows none|every model|
127
+
128
+ Any other value is a 400 naming the field. Provider views carry `dataZone` (effective), `euProcessing`, `allowedModels` and `modelIds`.
129
+
130
+ The zone rule is the host's: `filterRegistryByDataZone(registry, rule)` keeps what `rule` allows (`any` all; `eu_options` EU, local, and EU option with `eu_processing: true`; `eu` EU and local), and keeps a merged registry merged. `entryDataZone`, `entryAllowedByDataZone` and `dataZoneRefusal` answer per entry; `PRESET_DATA_ZONES` copies the `@skaile/provider-registry/catalog` zone per preset, so the panel needs no catalog in the browser. The vocabulary is the registry's: `AI_PROVIDER_DATA_ZONES`, `AI_PROVIDER_DATA_ZONE_RULES`, `AIProviderDataZone`, `AIProviderDataZoneRule`, and `isModelAllowed` / `allowedModelIds` for `allowed_models`, re-exported by `/server` and `/vue`. Pass the rule to `resolveHarnessProviders(..., { dataZoneRule })` and to `/workspaces`' `buildHarnessRows(..., { candidates, dataZoneRule })`: an entry the rule keeps out is reported as `blocked-by-rule` (`blocked`, `HarnessRow.blockedProviders`) with a message naming the rule, and is never eligible or chosen.
131
+
132
+ In the panel every row shows its zone chip (`provider-zone-<id>`) and, for `llm` / `embed` entries, a model chip (`provider-models-chip-<id>`: "All models" / "N models"). An own `eu_option` row offers "EU processing is on for this account" (`provider-eu-processing-<id>`). `:data-zone-rule="rule"` marks rows outside the rule with a muted note (`provider-blocked-<id>`). "Models allowed here" (`provider-models-<id>`) switches All models / Only these (`provider-models-mode-<id>`); Only these is a searchable checklist (`provider-models-search-<id>`, `provider-model-<id>-<modelId>`) of the entry's declared models or, for a discovery entry, of `client.models(id)`, and Save (`provider-models-save-<id>`) writes `allowed_models`.
133
+
91
134
  ## Model roles
92
135
 
93
136
  A *model role* is a named purpose a host picks a model for. The host declares roles and stores the assignments (role id to selector); this package supplies the vocabulary, validation, resolution and a picker.
@@ -108,7 +151,7 @@ const roles = [
108
151
  ]
109
152
  validateModelRoleAssignments(roles, assignments, registry) // [{ role, code, message }]: one issue per role id, host turns them into 400s
110
153
  resolveModelRole('assistant', { roles, assignments, registry }) // { kind: 'model', selector, entryId } | { kind: 'voice', entryId } | { kind: 'unassigned' }
111
- resolveRegistryLanguageModel('@assistant', { roles: modelRoleAliases(roles, assignments) })
154
+ resolveRegistryLanguageModel('@assistant', { registry, env, roles: modelRoleAliases(roles, assignments) })
112
155
  ```
113
156
 
114
157
  A dangling assignment throws `ModelResolutionError` (`unknown_selector`). `<ForgeModelRolesPanel :roles :assignments :client :read-only @update:assignments>` renders one row per enabled role (provider select, model select from `client.models`, free text when the provider cannot list models, Clear, a `status` slot per row). It never writes; the host saves the emitted assignments.
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@skaile/forge-settings",
3
3
  "configKey": "forgeSettings",
4
- "version": "1.3.3",
4
+ "version": "2.0.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.3",
7
7
  "unbuild": "3.6.1"
@@ -1,28 +1,33 @@
1
1
  /**
2
2
  * `@skaile/forge-settings/ai-sdk` — the AI SDK language model of a registry
3
- * selector, resolved by `@skaile/workspaces/ai-sdk` and gated by the host's
4
- * credential policy.
3
+ * selector, built by `@skaile/provider-registry/ai-sdk` against one registry
4
+ * snapshot the host passes, with the credential resolved under the host's
5
+ * policy.
5
6
  *
6
- * Workspaces owns endpoint, header and credential wiring for every surface;
7
- * this module only decides *whether* an entry may be resolved and against
8
- * *which* environment. It is not a service locator: the caller passes the
9
- * selector (role-to-selector mapping belongs to the host's settings document)
10
- * and the environment explicitly.
7
+ * The shared constructor owns endpoint, header and SDK wiring for every
8
+ * surface it supports; this module decides *whether* an entry may be resolved
9
+ * and supplies its credential — through the same policy-checked resolver model
10
+ * discovery uses, so a model that lists is a model that calls with the same
11
+ * key. It is not a service locator: the caller passes the selector
12
+ * (role-to-selector mapping belongs to the host's settings document), the
13
+ * registry and the environment explicitly. Nothing is read from disk here;
14
+ * a Skaile Workspaces host that resolves against a project's registry uses
15
+ * `resolveWorkspaceLanguageModel` from `@skaile/forge-settings/workspaces`.
11
16
  *
12
17
  * Provider SDKs (`@ai-sdk/anthropic`, `@ai-sdk/openai`,
13
- * `@ai-sdk/openai-compatible`) are optional peers of Workspaces: install the
14
- * one the selected surface needs.
18
+ * `@ai-sdk/openai-compatible`) are optional peers: install the one the
19
+ * selected surface needs.
15
20
  */
16
- import { type MergedAIProviderRegistry } from "@skaile/workspaces/core";
17
- import { type ResolvedLanguageModel } from "@skaile/workspaces/ai-sdk";
21
+ import { type MergedAIProviderRegistry } from "@skaile/provider-registry";
22
+ import { type ResolvedLanguageModel } from "@skaile/provider-registry/ai-sdk";
18
23
  import type { CredentialPolicy } from "../server/credential-policy.js";
19
- export { ModelResolutionError } from "../model/errors.js";
20
24
  export type { ModelResolutionCode } from "../model/errors.js";
25
+ export { ModelResolutionError } from "../model/errors.js";
21
26
  export type { ResolvedLanguageModel };
22
27
  /** Options for {@link resolveRegistryLanguageModel}. */
23
28
  export interface ResolveRegistryLanguageModelOptions {
24
- /** Workspace whose `.skaile/providers.yml` is the workspace layer. */
25
- projectDir: string;
29
+ /** The one merged registry snapshot the selector resolves against. */
30
+ registry: MergedAIProviderRegistry;
26
31
  /**
27
32
  * The environment `env:` credential references are read from. Required, so a
28
33
  * host never reads the ambient process by accident.
@@ -35,21 +40,23 @@ export interface ResolveRegistryLanguageModelOptions {
35
40
  * before anything is read; a refused one is a `policy_refused` failure.
36
41
  */
37
42
  policy?: CredentialPolicy;
38
- /**
39
- * An already merged (e.g. transform-proxy-routed) registry. When set,
40
- * `projectDir` is not read for providers, the policy check runs on this
41
- * registry, and it is handed to Workspaces as the registry to resolve against.
42
- */
43
- registry?: MergedAIProviderRegistry;
43
+ /** Local encrypted store for `keyring:` references; else `FORGE_CREDENTIAL_STORE` of `env`. */
44
+ path?: string;
45
+ /** 64-char hex key of the local store; else `FORGE_CREDENTIAL_KEY` of `env`. */
46
+ key?: string;
44
47
  }
45
48
  /**
46
49
  * The AI SDK model for `selector` (`<providerId>/<modelId>`, a bare model id or
47
- * `@role`), resolved by Workspaces against the registry on disk, or the given
48
- * `registry`.
50
+ * `@role`), resolved against `options.registry` only.
51
+ *
52
+ * Strict: a selector naming a provider that is missing from the registry's
53
+ * enabled `llm` entries (and that no entry maps as a model id), disabled, or an
54
+ * unassigned `@role` fails — nothing is rerouted to another endpoint and
55
+ * nothing is sent. A bare model id without `/` resolves as the registry orders it.
49
56
  *
50
- * @throws {ModelResolutionError} `registry_invalid` (only when read from disk) ·
51
- * `unknown_selector` · `policy_refused` · `unresolved` (credential, endpoint or
52
- * provider SDK; the message is fixed and never carries Workspaces' text, which
53
- * stays on `cause`).
57
+ * @throws {ModelResolutionError} `unknown_selector` (no enabled entry serves the
58
+ * selector, or the named entry is disabled / not an `llm` entry) ·
59
+ * `policy_refused` · `unresolved` (credential, endpoint or provider SDK; the
60
+ * message is fixed, the underlying error stays on `cause`).
54
61
  */
55
62
  export declare function resolveRegistryLanguageModel(selector: string, options: ResolveRegistryLanguageModelOptions): Promise<ResolvedLanguageModel>;
@@ -1,24 +1,50 @@
1
- import { parseModelRef } from "@skaile/workspaces/core";
2
- import { resolveLanguageModel } from "@skaile/workspaces/ai-sdk";
3
- import { loadMergedProviderRegistry } from "@skaile/workspaces/sdk/bridge";
4
- import { ModelResolutionError, assertEntryAllowed } from "../model/errors.js";
1
+ import { parseModelRef } from "@skaile/provider-registry";
2
+ import {
3
+ resolveLanguageModel
4
+ } from "@skaile/provider-registry/ai-sdk";
5
+ import { assertEntryAllowed, ModelResolutionError } from "../model/errors.js";
6
+ import { credentialStoreOptions, selectorProviderId } from "../model/selector.js";
7
+ import { resolveEntryCredential } from "../server/credential-store.js";
5
8
  export { ModelResolutionError } from "../model/errors.js";
9
+ function policyCredentialResolver(options) {
10
+ return (entry, providerId) => {
11
+ assertEntryAllowed(entry, providerId, options.policy);
12
+ const resolved = resolveEntryCredential(entry, credentialStoreOptions(options.env, options));
13
+ return resolved.error && !resolved.value ? { error: resolved.error } : { ...resolved.value !== void 0 ? { value: resolved.value } : {} };
14
+ };
15
+ }
6
16
  export async function resolveRegistryLanguageModel(selector, options) {
7
- const merged = options.registry ?? loadRegistry(options.projectDir);
17
+ const merged = options.registry;
18
+ const head = selectorProviderId(selector);
19
+ const named = head === void 0 ? void 0 : merged.providers[head];
20
+ if (head !== void 0 && named !== void 0 && (named.kind !== "llm" || named.enabled === false)) {
21
+ throw new ModelResolutionError(
22
+ "unknown_selector",
23
+ `provider '${head}' is ${named.kind !== "llm" ? "not an llm entry" : "disabled"}`,
24
+ { providerId: head }
25
+ );
26
+ }
8
27
  const ref = parseModelRef(selector, merged, options.roles);
9
- if (!ref) {
28
+ if (!ref || head !== void 0 && named === void 0 && ref.fallback === true) {
10
29
  throw new ModelResolutionError("unknown_selector", `no provider serves '${selector}'`);
11
30
  }
31
+ if (ref.role !== void 0 && !options.roles?.[ref.role]) {
32
+ throw new ModelResolutionError(
33
+ "unknown_selector",
34
+ `role '@${ref.role}' is not assigned to a model`
35
+ );
36
+ }
12
37
  const entry = merged.providers[ref.providerId];
13
38
  if (entry) assertEntryAllowed(entry, ref.providerId, options.policy);
14
39
  try {
15
40
  return await resolveLanguageModel(selector, {
16
- projectDir: options.projectDir,
41
+ registry: merged,
17
42
  env: options.env,
18
43
  ...options.roles ? { roles: options.roles } : {},
19
- ...options.registry ? { registry: options.registry } : {}
44
+ resolveCredential: policyCredentialResolver(options)
20
45
  });
21
46
  } catch (cause) {
47
+ if (cause instanceof ModelResolutionError) throw cause;
22
48
  throw new ModelResolutionError(
23
49
  "unresolved",
24
50
  `provider '${ref.providerId}': credential, endpoint or provider SDK could not be resolved`,
@@ -26,11 +52,3 @@ export async function resolveRegistryLanguageModel(selector, options) {
26
52
  );
27
53
  }
28
54
  }
29
- function loadRegistry(projectDir) {
30
- const { merged, diagnostics } = loadMergedProviderRegistry({ projectDir });
31
- const errors = diagnostics.filter((d) => d.severity === "error").map((d) => d.path ? `${d.path}: ${d.message}` : d.message);
32
- if (errors.length > 0) {
33
- throw new ModelResolutionError("registry_invalid", `providers registry: ${errors.join("; ")}`);
34
- }
35
- return merged;
36
- }
@@ -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>;