@skaile/forge-settings 2.0.2 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - ce96a12: The registry model adapter no longer depends on `@skaile/forge-editor`. It moves to the editor-neutral entry `/model-provider` and declares its own `ModelProfile`, `ModelRef` and `ModelProvider` types, which match the editor's `models` port structurally; a host assigns `createModelProvider(...)` to `ports.models` and its typecheck checks the fit. `@skaile/forge-editor` is no longer a peer or dev dependency.
8
+
9
+ `/editor` stays as a deprecated entry with the old names and goes with the next major. Migrate to `/model-provider`:
10
+
11
+ - `createEditorModelProvider(options)` → `createModelProvider(options)`
12
+ - `editorLanguageModel(registry, env, ref, options)` → `registryLanguageModel(registry, env, ref, options)`
13
+ - `listEditorModelProfiles(registry, env, options)` → `listModelProfiles(registry, env, options)`
14
+ - `editorModelProfiles(registry, options)` → `declaredModelProfiles(registry, options)`
15
+ - Types `EditorModelPolicyOptions` → `ModelPolicyOptions`, `EditorModelProviderOptions` → `ModelProviderOptions`; `ModelProfile`, `ModelRef` and `ModelProvider` come from `/model-provider` instead of `@skaile/forge-editor/ai`.
16
+
3
17
  ## 2.0.2
4
18
 
5
19
  ### Patch Changes
package/README.md CHANGED
@@ -8,10 +8,11 @@ Provider and settings operations for Skaile forge apps, without the app's databa
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`, the `@ai-sdk/*` provider the surface needs |
11
+ | `/model-provider` | a `ModelProvider` (`listModels`, `languageModel`) over a registry, with its own `ModelProfile` / `ModelRef` types | `ai` + the `@ai-sdk/*` provider the surface needs |
12
+ | `/editor` | deprecated: the `/model-provider` exports under their 2.0 names (`createEditorModelProvider`, `editorModelProfiles`, `listEditorModelProfiles`, `editorLanguageModel`); removed in the next major | as `/model-provider` |
12
13
  | `/workspaces` | Skaile Workspaces integration: `SkaileSettings`, `SETTINGS_DEFAULTS`, legacy settings migrations, harness settings rules, `workspaceCredentialStorePath`, `resolveWorkspaceLanguageModel`, `readWorkspaceModelEndpoint` | `@skaile/workspaces` |
13
14
 
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.
15
+ 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 `/model-provider` install, load and typecheck without `@skaile/workspaces`; only `/workspaces` needs it.
15
16
 
16
17
  ## What stays with the host
17
18
 
@@ -50,13 +51,13 @@ There is no implicit store location. The file is `options.path`, else `FORGE_CRE
50
51
 
51
52
  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.
52
53
 
53
- ## Models for the editor and the AI SDK
54
+ ## Model provider and the AI SDK
54
55
 
55
56
  ```ts
56
- // server/utils/editor-models.ts: ModelProvider for @skaile/forge-editor
57
- import { createEditorModelProvider, ModelResolutionError } from '@skaile/forge-settings/editor'
57
+ // server/utils/models.ts: the host's model provider over its registry
58
+ import { createModelProvider, ModelResolutionError } from '@skaile/forge-settings/model-provider'
58
59
 
59
- export const models = createEditorModelProvider({
60
+ export const models = createModelProvider({
60
61
  registry: () => getProviderRegistry(home()),
61
62
  env: () => effectiveEnv(),
62
63
  policy, // refused entries are neither listed nor resolved
@@ -65,7 +66,9 @@ export const models = createEditorModelProvider({
65
66
  })
66
67
  ```
67
68
 
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
+ The entry defines its own `ModelProfile`, `ModelRef` and `ModelProvider` types and depends on no consumer. They match a host's model port structurally (for example an editor's `models` port, a chat handler or an agent runtime): the host assigns `createModelProvider(...)` to its port, and its own typecheck verifies the fit.
70
+
71
+ `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 host's own models. `declaredModelProfiles(registry, { policy? })` is the declared-only, network-free list; `await listModelProfiles(registry, env, options)` the declared and discovered one; `await registryLanguageModel(registry, env, ref, options)` is the adapter's model lookup.
69
72
 
70
73
  ```ts
71
74
  import { resolveRegistryLanguageModel } from '@skaile/forge-settings/ai-sdk'
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@skaile/forge-settings",
3
3
  "configKey": "forgeSettings",
4
- "version": "2.0.2",
4
+ "version": "2.1.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.3",
7
7
  "unbuild": "3.6.1"
@@ -1,97 +1,23 @@
1
1
  /**
2
- * `@skaile/forge-settings/editor` — the editor's `models` port over a provider
3
- * registry.
2
+ * `@skaile/forge-settings/editor` - the former name of
3
+ * `@skaile/forge-settings/model-provider`, kept so 2.x hosts keep working.
4
4
  *
5
- * `@skaile/forge-editor`'s AI routes ask the host's `ModelProvider`
6
- * (`listModels` / `languageModel`) before their own runtime-config presets.
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`.
25
- *
26
- * The registry and the credential environment are supplied by the host on every
27
- * call, so provider edits apply without a restart and nothing reads the ambient
28
- * process. Failures are typed ({@link ModelResolutionError}); the host maps
29
- * them to HTTP in `onFailure`.
30
- *
31
- * `@skaile/forge-editor` is an optional peer of this entry only, for its
32
- * `ModelProvider` contract (types).
5
+ * Every export is the `/model-provider` one under its old name. New code
6
+ * imports `/model-provider`; this entry goes with the next major.
33
7
  */
34
- import type { ModelProfile, ModelRef } from "@skaile/forge-editor/ai";
35
- import type { ModelProvider } from "@skaile/forge-editor/server";
36
- import { type LanguageModelObject } from "@skaile/provider-registry/ai-sdk";
37
- import { ModelResolutionError } from "../model/errors.js";
38
- import { type CredentialPolicy } from "../server/credential-policy.js";
39
- import { type AIProviderRegistryView } from "../shared/provider-registry.js";
8
+ import { createModelProvider, declaredModelProfiles, listModelProfiles, type ModelPolicyOptions, type ModelProviderOptions, registryLanguageModel } from "../model-provider/index.js";
40
9
  export type { ModelResolutionCode } from "../model/errors.js";
41
10
  export { ModelResolutionError } from "../model/errors.js";
42
- /** Policy and credential store for an editor model call; see `CredentialPolicy`. */
43
- export interface EditorModelPolicyOptions {
44
- /** Which credential references may be resolved; refused entries are not offered. */
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;
50
- }
51
- /**
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.
56
- */
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[]>;
70
- /**
71
- * The language model of selector `ref.preset`, or `null` when the registry does
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.
79
- *
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.
85
- */
86
- export declare function editorLanguageModel(registry: AIProviderRegistryView, env: Record<string, string | undefined>, ref: ModelRef, options?: EditorModelPolicyOptions): Promise<LanguageModelObject | null>;
87
- /** Options for {@link createEditorModelProvider}. */
88
- export interface EditorModelProviderOptions extends EditorModelPolicyOptions {
89
- /** The registry to serve, read on every call. */
90
- registry: () => AIProviderRegistryView | Promise<AIProviderRegistryView>;
91
- /** The environment `env:` references resolve against, read on every call. */
92
- env: () => Record<string, string | undefined> | Promise<Record<string, string | undefined>>;
93
- /** Turn a typed failure into the host's own error (e.g. its HTTP error). Default: rethrow as is. */
94
- onFailure?: (error: ModelResolutionError) => Error;
95
- }
96
- /** The editor's `models` port over the host's registry. */
97
- export declare function createEditorModelProvider(options: EditorModelProviderOptions): ModelProvider;
11
+ export type { ModelProfile, ModelProvider, ModelRef } from "../model-provider/index.js";
12
+ /** @deprecated Use `ModelPolicyOptions` from `@skaile/forge-settings/model-provider`. */
13
+ export type EditorModelPolicyOptions = ModelPolicyOptions;
14
+ /** @deprecated Use `ModelProviderOptions` from `@skaile/forge-settings/model-provider`. */
15
+ export type EditorModelProviderOptions = ModelProviderOptions;
16
+ /** @deprecated Use `declaredModelProfiles` from `@skaile/forge-settings/model-provider`. */
17
+ export declare const editorModelProfiles: typeof declaredModelProfiles;
18
+ /** @deprecated Use `listModelProfiles` from `@skaile/forge-settings/model-provider`. */
19
+ export declare const listEditorModelProfiles: typeof listModelProfiles;
20
+ /** @deprecated Use `registryLanguageModel` from `@skaile/forge-settings/model-provider`. */
21
+ export declare const editorLanguageModel: typeof registryLanguageModel;
22
+ /** @deprecated Use `createModelProvider` from `@skaile/forge-settings/model-provider`. */
23
+ export declare const createEditorModelProvider: typeof createModelProvider;
@@ -1,114 +1,11 @@
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";
6
- import { entryPolicyRefusal } from "../server/credential-policy.js";
7
- import { discoverProviderModels } from "../server/provider-discovery.js";
8
- import { listAIProviderModels } from "../server/provider-service.js";
9
1
  import {
10
- asMergedRegistry,
11
- entriesOfKind
12
- } from "../shared/provider-registry.js";
2
+ createModelProvider,
3
+ declaredModelProfiles,
4
+ listModelProfiles,
5
+ registryLanguageModel
6
+ } from "../model-provider/index.js";
13
7
  export { ModelResolutionError } from "../model/errors.js";
14
- function editorEntries(registry, policy) {
15
- const entries = /* @__PURE__ */ new Map();
16
- for (const [id, entry] of entriesOfKind(registry, "llm")) {
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);
22
- }
23
- return entries;
24
- }
25
- function registryProfile(providerId, entry, model) {
26
- return {
27
- id: model.id,
28
- label: `${providerId} \xB7 ${model.name}`,
29
- endpoint: resolveEntryBaseUrl(entry),
30
- model: model.providerModelId,
31
- source: "preset"
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();
52
- }
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 = {}) {
84
- if (!("preset" in ref)) return null;
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)
91
- });
92
- return resolved.model;
93
- }
94
- export function createEditorModelProvider(options) {
95
- return {
96
- async languageModel(ref) {
97
- try {
98
- return await editorLanguageModel(
99
- await options.registry(),
100
- await options.env(),
101
- ref,
102
- options
103
- );
104
- } catch (error) {
105
- if (error instanceof ModelResolutionError && options.onFailure)
106
- throw options.onFailure(error);
107
- throw error;
108
- }
109
- },
110
- async listModels() {
111
- return listEditorModelProfiles(await options.registry(), await options.env(), options);
112
- }
113
- };
114
- }
8
+ export const editorModelProfiles = declaredModelProfiles;
9
+ export const listEditorModelProfiles = listModelProfiles;
10
+ export const editorLanguageModel = registryLanguageModel;
11
+ export const createEditorModelProvider = createModelProvider;
@@ -0,0 +1,118 @@
1
+ /**
2
+ * `@skaile/forge-settings/model-provider` — a model provider over a provider
3
+ * registry: the models a host offers (`listModels`) and the language model of
4
+ * a model reference (`languageModel`).
5
+ *
6
+ * Its shape is the one a host's model port expects (the editor's `models`
7
+ * port, a chat handler, an agent runtime): the types below are this entry's
8
+ * own and match such a port structurally, so the host assigns
9
+ * `createModelProvider(...)` to it and its typecheck verifies the fit.
10
+ *
11
+ * The adapter is a thin layer over `@skaile/forge-settings/ai-sdk`: a
12
+ * profile's id is the registry selector `<providerId>/<modelId>`, which
13
+ * `languageModel` receives back as `{ preset: id }` and resolves through the
14
+ * same constructor, credential resolver and policy as every other registry
15
+ * model. Every `llm` entry whose surface the AI SDK constructor speaks
16
+ * (`openai-completions`, `openai-responses`, `anthropic-messages`) and that has
17
+ * an endpoint is offered.
18
+ *
19
+ * Listing combines what the registry declares (`model_map` / `capabilities`)
20
+ * with what an entry's own models list reports, for entries that declare
21
+ * `discovery` or declare no models at all. Declared models work without a
22
+ * models list; a declared identity wins over a discovered one.
23
+ *
24
+ * The registry is authoritative for the selectors it owns: an empty registry
25
+ * lists `[]`, and a `<providerId>/…` selector naming a registry entry that is
26
+ * disabled, refused or unresolvable throws instead of falling through to the
27
+ * host's own models. Only a selector the registry does not own answers `null`.
28
+ *
29
+ * The registry and the credential environment are supplied by the host on every
30
+ * call, so provider edits apply without a restart and nothing reads the ambient
31
+ * process. Failures are typed ({@link ModelResolutionError}); the host maps
32
+ * them to HTTP in `onFailure`.
33
+ */
34
+ import { type LanguageModelObject } from "@skaile/provider-registry/ai-sdk";
35
+ import { ModelResolutionError } from "../model/errors.js";
36
+ import { type CredentialPolicy } from "../server/credential-policy.js";
37
+ import { type AIProviderRegistryView } from "../shared/provider-registry.js";
38
+ export type { ModelResolutionCode } from "../model/errors.js";
39
+ export { ModelResolutionError } from "../model/errors.js";
40
+ /** A model as a host offers it; `id` is what `languageModel` receives back as `{ preset: id }`. Never carries a key. */
41
+ export interface ModelProfile {
42
+ id: string;
43
+ label: string;
44
+ endpoint: string;
45
+ model: string;
46
+ source: "preset" | "user";
47
+ }
48
+ /** The model a request runs on: a preset (registry selector) by id, or an OpenAI-compatible endpoint with its own key. */
49
+ export type ModelRef = {
50
+ preset: string;
51
+ } | {
52
+ endpoint: string;
53
+ model: string;
54
+ apiKey: string;
55
+ };
56
+ /** The provider {@link createModelProvider} returns. */
57
+ export interface ModelProvider {
58
+ /** The language model of `ref`; `null` when the registry does not own it. */
59
+ languageModel(ref: ModelRef): Promise<LanguageModelObject | null>;
60
+ /** The registry's declared and discovered models as profiles. */
61
+ listModels(): Promise<ModelProfile[]>;
62
+ }
63
+ /** Policy and credential store for a model call; see `CredentialPolicy`. */
64
+ export interface ModelPolicyOptions {
65
+ /** Which credential references may be resolved; refused entries are not offered. */
66
+ policy?: CredentialPolicy;
67
+ /** Local encrypted store for `keyring:` references; else `FORGE_CREDENTIAL_STORE` of the env. */
68
+ path?: string;
69
+ /** 64-char hex key of the local store; else `FORGE_CREDENTIAL_KEY` of the env. */
70
+ key?: string;
71
+ }
72
+ /**
73
+ * The models the registry declares (`model_map` / `capabilities`) as model
74
+ * profiles, in registry order — no network. With a policy, entries it
75
+ * refuses are left out. {@link createModelProvider} lists these plus the
76
+ * models discovered live.
77
+ */
78
+ export declare function declaredModelProfiles(registry: AIProviderRegistryView, options?: ModelPolicyOptions): ModelProfile[];
79
+ /**
80
+ * Declared and discovered models as profiles, in registry order, one
81
+ * profile per selector.
82
+ *
83
+ * An entry's models list is asked when the entry declares `discovery` (and not
84
+ * `type: none`) or declares no models; it is asked with the credential the
85
+ * policy allows, exactly as inference resolves it. A failed or unsupported
86
+ * list leaves that entry with its declared models. A discovered id that a
87
+ * declared model already names (as selector or as mapped provider id) is
88
+ * dropped: the declared identity wins.
89
+ */
90
+ export declare function listModelProfiles(registry: AIProviderRegistryView, env: Record<string, string | undefined>, options?: ModelPolicyOptions): Promise<ModelProfile[]>;
91
+ /**
92
+ * The language model of selector `ref.preset`, or `null` when the registry does
93
+ * not own it: a browser-defined `{ endpoint, model, apiKey }` reference, or a
94
+ * preset id whose leading segment names no registry entry, is the host's own
95
+ * to resolve.
96
+ *
97
+ * Delegates to `resolveRegistryLanguageModel` (`@skaile/forge-settings/ai-sdk`),
98
+ * so the selector's model id may be declared or discovered: an id the entry's
99
+ * `model_map` maps is sent as the mapped id, any other is sent verbatim.
100
+ *
101
+ * @throws {ModelResolutionError} for an owned selector: `unknown_selector` when
102
+ * the entry is disabled or not an `llm` entry, `policy_refused` when the
103
+ * policy refuses it (checked before its credential is read), `unresolved`
104
+ * when its credential, endpoint or SDK does not resolve. No message carries a
105
+ * secret.
106
+ */
107
+ export declare function registryLanguageModel(registry: AIProviderRegistryView, env: Record<string, string | undefined>, ref: ModelRef, options?: ModelPolicyOptions): Promise<LanguageModelObject | null>;
108
+ /** Options for {@link createModelProvider}. */
109
+ export interface ModelProviderOptions extends ModelPolicyOptions {
110
+ /** The registry to serve, read on every call. */
111
+ registry: () => AIProviderRegistryView | Promise<AIProviderRegistryView>;
112
+ /** The environment `env:` references resolve against, read on every call. */
113
+ env: () => Record<string, string | undefined> | Promise<Record<string, string | undefined>>;
114
+ /** Turn a typed failure into the host's own error (e.g. its HTTP error). Default: rethrow as is. */
115
+ onFailure?: (error: ModelResolutionError) => Error;
116
+ }
117
+ /** A model provider over the host's registry, for a host's model port (e.g. the editor's `models`). */
118
+ export declare function createModelProvider(options: ModelProviderOptions): ModelProvider;
@@ -0,0 +1,114 @@
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";
6
+ import { entryPolicyRefusal } from "../server/credential-policy.js";
7
+ import { discoverProviderModels } from "../server/provider-discovery.js";
8
+ import { listAIProviderModels } from "../server/provider-service.js";
9
+ import {
10
+ asMergedRegistry,
11
+ entriesOfKind
12
+ } from "../shared/provider-registry.js";
13
+ export { ModelResolutionError } from "../model/errors.js";
14
+ function servedEntries(registry, policy) {
15
+ const entries = /* @__PURE__ */ new Map();
16
+ for (const [id, entry] of entriesOfKind(registry, "llm")) {
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);
22
+ }
23
+ return entries;
24
+ }
25
+ function registryProfile(providerId, entry, model) {
26
+ return {
27
+ id: model.id,
28
+ label: `${providerId} \xB7 ${model.name}`,
29
+ endpoint: resolveEntryBaseUrl(entry),
30
+ model: model.providerModelId,
31
+ source: "preset"
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 declaredModelProfiles(registry, options = {}) {
51
+ return [...declaredProfiles(registry, servedEntries(registry, options.policy)).values()].flat();
52
+ }
53
+ export async function listModelProfiles(registry, env, options = {}) {
54
+ const entries = servedEntries(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 registryLanguageModel(registry, env, ref, options = {}) {
84
+ if (!("preset" in ref)) return null;
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)
91
+ });
92
+ return resolved.model;
93
+ }
94
+ export function createModelProvider(options) {
95
+ return {
96
+ async languageModel(ref) {
97
+ try {
98
+ return await registryLanguageModel(
99
+ await options.registry(),
100
+ await options.env(),
101
+ ref,
102
+ options
103
+ );
104
+ } catch (error) {
105
+ if (error instanceof ModelResolutionError && options.onFailure)
106
+ throw options.onFailure(error);
107
+ throw error;
108
+ }
109
+ },
110
+ async listModels() {
111
+ return listModelProfiles(await options.registry(), await options.env(), options);
112
+ }
113
+ };
114
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@skaile/forge-settings",
3
- "version": "2.0.2",
4
- "description": "Provider and settings operations for Skaile forge apps: provider registry layers, credential store, env layers, model discovery, the providers panel, and editor / AI SDK model adapters.",
3
+ "version": "2.1.0",
4
+ "description": "Provider and settings operations for Skaile forge apps: provider registry layers, credential store, env layers, model discovery, the providers panel, and model provider / AI SDK model adapters.",
5
5
  "type": "module",
6
6
  "main": "./dist/module.mjs",
7
7
  "types": "./dist/types.d.mts",
@@ -22,6 +22,10 @@
22
22
  "types": "./dist/runtime/ai-sdk/index.d.ts",
23
23
  "import": "./dist/runtime/ai-sdk/index.js"
24
24
  },
25
+ "./model-provider": {
26
+ "types": "./dist/runtime/model-provider/index.d.ts",
27
+ "import": "./dist/runtime/model-provider/index.js"
28
+ },
25
29
  "./editor": {
26
30
  "types": "./dist/runtime/editor/index.d.ts",
27
31
  "import": "./dist/runtime/editor/index.js"
@@ -68,7 +72,6 @@
68
72
  "@ai-sdk/openai": "^4.0.0",
69
73
  "@ai-sdk/openai-compatible": "^3.0.0",
70
74
  "@nuxt/ui": "^4.11.1",
71
- "@skaile/forge-editor": "^8.0.0",
72
75
  "@skaile/workspaces": "^7.8.0 || ^8.0.0 || ^9.0.0 || ^10.0.0",
73
76
  "ai": "^7.0.0",
74
77
  "nuxt": "^4.0.0",
@@ -87,9 +90,6 @@
87
90
  "@nuxt/ui": {
88
91
  "optional": true
89
92
  },
90
- "@skaile/forge-editor": {
91
- "optional": true
92
- },
93
93
  "@skaile/workspaces": {
94
94
  "optional": true
95
95
  },
@@ -109,7 +109,6 @@
109
109
  "@nuxt/module-builder": "^1.0.0",
110
110
  "@nuxt/schema": "^4.0.0",
111
111
  "@nuxt/ui": "^4.11.1",
112
- "@skaile/forge-editor": "^8.0.0",
113
112
  "@skaile/workspaces": "^10.3.0",
114
113
  "@types/bun": "^1.2.0",
115
114
  "@types/node": "^22.0.0",
@@ -1,241 +1,32 @@
1
1
  /**
2
- * `@skaile/forge-settings/editor` — the editor's `models` port over a provider
3
- * registry.
2
+ * `@skaile/forge-settings/editor` - the former name of
3
+ * `@skaile/forge-settings/model-provider`, kept so 2.x hosts keep working.
4
4
  *
5
- * `@skaile/forge-editor`'s AI routes ask the host's `ModelProvider`
6
- * (`listModels` / `languageModel`) before their own runtime-config presets.
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`.
25
- *
26
- * The registry and the credential environment are supplied by the host on every
27
- * call, so provider edits apply without a restart and nothing reads the ambient
28
- * process. Failures are typed ({@link ModelResolutionError}); the host maps
29
- * them to HTTP in `onFailure`.
30
- *
31
- * `@skaile/forge-editor` is an optional peer of this entry only, for its
32
- * `ModelProvider` contract (types).
5
+ * Every export is the `/model-provider` one under its old name. New code
6
+ * imports `/model-provider`; this entry goes with the next major.
33
7
  */
34
-
35
- import type { ModelProfile, ModelRef } from "@skaile/forge-editor/ai";
36
- import type { ModelProvider } from "@skaile/forge-editor/server";
37
- import { resolveEntryBaseUrl } from "@skaile/provider-registry";
38
- import { AI_SDK_SURFACES, type LanguageModelObject } from "@skaile/provider-registry/ai-sdk";
39
- import { resolveRegistryLanguageModel } from "../ai-sdk";
40
- import { ModelResolutionError } from "../model/errors";
41
- import { credentialStoreOptions, selectorProviderId } from "../model/selector";
42
- import { type CredentialPolicy, entryPolicyRefusal } from "../server/credential-policy";
43
- import { discoverProviderModels } from "../server/provider-discovery";
44
- import { listAIProviderModels } from "../server/provider-service";
45
8
  import {
46
- type AIProviderLlmEntry,
47
- type AIProviderRegistryView,
48
- asMergedRegistry,
49
- entriesOfKind,
50
- } from "../shared/provider-registry";
9
+ createModelProvider,
10
+ declaredModelProfiles,
11
+ listModelProfiles,
12
+ type ModelPolicyOptions,
13
+ type ModelProviderOptions,
14
+ registryLanguageModel,
15
+ } from "../model-provider";
51
16
 
52
17
  export type { ModelResolutionCode } from "../model/errors";
53
18
  export { ModelResolutionError } from "../model/errors";
54
-
55
- /** Policy and credential store for an editor model call; see `CredentialPolicy`. */
56
- export interface EditorModelPolicyOptions {
57
- /** Which credential references may be resolved; refused entries are not offered. */
58
- policy?: CredentialPolicy;
59
- /** Local encrypted store for `keyring:` references; else `FORGE_CREDENTIAL_STORE` of the env. */
60
- path?: string;
61
- /** 64-char hex key of the local store; else `FORGE_CREDENTIAL_KEY` of the env. */
62
- key?: string;
63
- }
64
-
65
- /** The entries the AI SDK constructor can serve (surface and endpoint), by id; policy-refused ones left out. */
66
- function editorEntries(
67
- registry: AIProviderRegistryView,
68
- policy: CredentialPolicy | undefined,
69
- ): Map<string, AIProviderLlmEntry> {
70
- const entries = new Map<string, AIProviderLlmEntry>();
71
- for (const [id, entry] of entriesOfKind(registry, "llm")) {
72
- const surface = entry.surface ?? "openai-completions";
73
- if (!(AI_SDK_SURFACES as readonly string[]).includes(surface)) continue;
74
- if (!resolveEntryBaseUrl(entry)) continue;
75
- if (entryPolicyRefusal(entry, policy)) continue;
76
- entries.set(id, entry);
77
- }
78
- return entries;
79
- }
80
-
81
- /** One registry model as an editor profile: its selector `id`, shown as `<providerId> · <name>`. */
82
- function registryProfile(
83
- providerId: string,
84
- entry: AIProviderLlmEntry,
85
- model: { id: string; name: string; providerModelId: string },
86
- ): ModelProfile {
87
- return {
88
- id: model.id,
89
- label: `${providerId} · ${model.name}`,
90
- endpoint: resolveEntryBaseUrl(entry)!,
91
- model: model.providerModelId,
92
- source: "preset",
93
- };
94
- }
95
-
96
- /** The declared models of the served `entries` as profiles, grouped by provider id in registry order. */
97
- function declaredProfiles(
98
- registry: AIProviderRegistryView,
99
- entries: Map<string, AIProviderLlmEntry>,
100
- ): Map<string, ModelProfile[]> {
101
- const byProvider = new Map<string, ModelProfile[]>();
102
- for (const model of listAIProviderModels(registry)) {
103
- const entry = entries.get(model.providerId);
104
- if (!entry) continue;
105
- const profile = registryProfile(model.providerId, entry, {
106
- id: model.id,
107
- name: model.modelId,
108
- providerModelId: model.providerModelId,
109
- });
110
- const own = byProvider.get(model.providerId);
111
- if (own) own.push(profile);
112
- else byProvider.set(model.providerId, [profile]);
113
- }
114
- return byProvider;
115
- }
116
-
117
- /**
118
- * The models the registry declares (`model_map` / `capabilities`) as editor
119
- * model profiles, in registry order — no network. With a policy, entries it
120
- * refuses are left out. {@link createEditorModelProvider} lists these plus the
121
- * models discovered live.
122
- */
123
- export function editorModelProfiles(
124
- registry: AIProviderRegistryView,
125
- options: EditorModelPolicyOptions = {},
126
- ): ModelProfile[] {
127
- return [...declaredProfiles(registry, editorEntries(registry, options.policy)).values()].flat();
128
- }
129
-
130
- /**
131
- * Declared and discovered models as editor profiles, in registry order, one
132
- * profile per selector.
133
- *
134
- * An entry's models list is asked when the entry declares `discovery` (and not
135
- * `type: none`) or declares no models; it is asked with the credential the
136
- * policy allows, exactly as inference resolves it. A failed or unsupported
137
- * list leaves that entry with its declared models. A discovered id that a
138
- * declared model already names (as selector or as mapped provider id) is
139
- * dropped: the declared identity wins.
140
- */
141
- export async function listEditorModelProfiles(
142
- registry: AIProviderRegistryView,
143
- env: Record<string, string | undefined>,
144
- options: EditorModelPolicyOptions = {},
145
- ): Promise<ModelProfile[]> {
146
- const entries = editorEntries(registry, options.policy);
147
- const declared = declaredProfiles(registry, entries);
148
- const listed = await Promise.all(
149
- [...entries].map(async ([providerId, entry]): Promise<ModelProfile[]> => {
150
- const own = declared.get(providerId) ?? [];
151
- const listable =
152
- entry.discovery !== undefined ? entry.discovery.type !== "none" : own.length === 0;
153
- if (!listable) return own;
154
- const result = await discoverProviderModels(
155
- providerId,
156
- registry,
157
- credentialStoreOptions(env, options),
158
- );
159
- if (result.status !== "live") return own;
160
- const taken = new Set(
161
- own.flatMap((profile) => [profile.id, `${providerId}/${profile.model}`]),
162
- );
163
- const discovered = result.models.flatMap((model) => {
164
- const id = `${providerId}/${model.id}`;
165
- if (taken.has(id)) return [];
166
- taken.add(id);
167
- return [
168
- registryProfile(providerId, entry, { id, name: model.name, providerModelId: model.id }),
169
- ];
170
- });
171
- return [...own, ...discovered];
172
- }),
173
- );
174
- return listed.flat();
175
- }
176
-
177
- /**
178
- * The language model of selector `ref.preset`, or `null` when the registry does
179
- * not own it: a browser-defined `{ endpoint, model, apiKey }` reference, or a
180
- * preset id whose leading segment names no registry entry, is the editor's own
181
- * to resolve.
182
- *
183
- * Delegates to `resolveRegistryLanguageModel` (`@skaile/forge-settings/ai-sdk`),
184
- * so the selector's model id may be declared or discovered: an id the entry's
185
- * `model_map` maps is sent as the mapped id, any other is sent verbatim.
186
- *
187
- * @throws {ModelResolutionError} for an owned selector: `unknown_selector` when
188
- * the entry is disabled or not an `llm` entry, `policy_refused` when the
189
- * policy refuses it (checked before its credential is read), `unresolved`
190
- * when its credential, endpoint or SDK does not resolve. No message carries a
191
- * secret.
192
- */
193
- export async function editorLanguageModel(
194
- registry: AIProviderRegistryView,
195
- env: Record<string, string | undefined>,
196
- ref: ModelRef,
197
- options: EditorModelPolicyOptions = {},
198
- ): Promise<LanguageModelObject | null> {
199
- if (!("preset" in ref)) return null;
200
- const merged = asMergedRegistry(registry);
201
- const head = selectorProviderId(ref.preset);
202
- if (head === undefined || merged.providers[head] === undefined) return null;
203
- const resolved = await resolveRegistryLanguageModel(ref.preset, {
204
- registry: merged,
205
- ...credentialStoreOptions(env, options),
206
- });
207
- return resolved.model;
208
- }
209
-
210
- /** Options for {@link createEditorModelProvider}. */
211
- export interface EditorModelProviderOptions extends EditorModelPolicyOptions {
212
- /** The registry to serve, read on every call. */
213
- registry: () => AIProviderRegistryView | Promise<AIProviderRegistryView>;
214
- /** The environment `env:` references resolve against, read on every call. */
215
- env: () => Record<string, string | undefined> | Promise<Record<string, string | undefined>>;
216
- /** Turn a typed failure into the host's own error (e.g. its HTTP error). Default: rethrow as is. */
217
- onFailure?: (error: ModelResolutionError) => Error;
218
- }
219
-
220
- /** The editor's `models` port over the host's registry. */
221
- export function createEditorModelProvider(options: EditorModelProviderOptions): ModelProvider {
222
- return {
223
- async languageModel(ref) {
224
- try {
225
- return await editorLanguageModel(
226
- await options.registry(),
227
- await options.env(),
228
- ref,
229
- options,
230
- );
231
- } catch (error) {
232
- if (error instanceof ModelResolutionError && options.onFailure)
233
- throw options.onFailure(error);
234
- throw error;
235
- }
236
- },
237
- async listModels() {
238
- return listEditorModelProfiles(await options.registry(), await options.env(), options);
239
- },
240
- };
241
- }
19
+ export type { ModelProfile, ModelProvider, ModelRef } from "../model-provider";
20
+
21
+ /** @deprecated Use `ModelPolicyOptions` from `@skaile/forge-settings/model-provider`. */
22
+ export type EditorModelPolicyOptions = ModelPolicyOptions;
23
+ /** @deprecated Use `ModelProviderOptions` from `@skaile/forge-settings/model-provider`. */
24
+ export type EditorModelProviderOptions = ModelProviderOptions;
25
+ /** @deprecated Use `declaredModelProfiles` from `@skaile/forge-settings/model-provider`. */
26
+ export const editorModelProfiles = declaredModelProfiles;
27
+ /** @deprecated Use `listModelProfiles` from `@skaile/forge-settings/model-provider`. */
28
+ export const listEditorModelProfiles = listModelProfiles;
29
+ /** @deprecated Use `registryLanguageModel` from `@skaile/forge-settings/model-provider`. */
30
+ export const editorLanguageModel = registryLanguageModel;
31
+ /** @deprecated Use `createModelProvider` from `@skaile/forge-settings/model-provider`. */
32
+ export const createEditorModelProvider = createModelProvider;
@@ -0,0 +1,259 @@
1
+ /**
2
+ * `@skaile/forge-settings/model-provider` — a model provider over a provider
3
+ * registry: the models a host offers (`listModels`) and the language model of
4
+ * a model reference (`languageModel`).
5
+ *
6
+ * Its shape is the one a host's model port expects (the editor's `models`
7
+ * port, a chat handler, an agent runtime): the types below are this entry's
8
+ * own and match such a port structurally, so the host assigns
9
+ * `createModelProvider(...)` to it and its typecheck verifies the fit.
10
+ *
11
+ * The adapter is a thin layer over `@skaile/forge-settings/ai-sdk`: a
12
+ * profile's id is the registry selector `<providerId>/<modelId>`, which
13
+ * `languageModel` receives back as `{ preset: id }` and resolves through the
14
+ * same constructor, credential resolver and policy as every other registry
15
+ * model. Every `llm` entry whose surface the AI SDK constructor speaks
16
+ * (`openai-completions`, `openai-responses`, `anthropic-messages`) and that has
17
+ * an endpoint is offered.
18
+ *
19
+ * Listing combines what the registry declares (`model_map` / `capabilities`)
20
+ * with what an entry's own models list reports, for entries that declare
21
+ * `discovery` or declare no models at all. Declared models work without a
22
+ * models list; a declared identity wins over a discovered one.
23
+ *
24
+ * The registry is authoritative for the selectors it owns: an empty registry
25
+ * lists `[]`, and a `<providerId>/…` selector naming a registry entry that is
26
+ * disabled, refused or unresolvable throws instead of falling through to the
27
+ * host's own models. Only a selector the registry does not own answers `null`.
28
+ *
29
+ * The registry and the credential environment are supplied by the host on every
30
+ * call, so provider edits apply without a restart and nothing reads the ambient
31
+ * process. Failures are typed ({@link ModelResolutionError}); the host maps
32
+ * them to HTTP in `onFailure`.
33
+ */
34
+
35
+ import { resolveEntryBaseUrl } from "@skaile/provider-registry";
36
+ import { AI_SDK_SURFACES, type LanguageModelObject } from "@skaile/provider-registry/ai-sdk";
37
+ import { resolveRegistryLanguageModel } from "../ai-sdk";
38
+ import { ModelResolutionError } from "../model/errors";
39
+ import { credentialStoreOptions, selectorProviderId } from "../model/selector";
40
+ import { type CredentialPolicy, entryPolicyRefusal } from "../server/credential-policy";
41
+ import { discoverProviderModels } from "../server/provider-discovery";
42
+ import { listAIProviderModels } from "../server/provider-service";
43
+ import {
44
+ type AIProviderLlmEntry,
45
+ type AIProviderRegistryView,
46
+ asMergedRegistry,
47
+ entriesOfKind,
48
+ } from "../shared/provider-registry";
49
+
50
+ export type { ModelResolutionCode } from "../model/errors";
51
+ export { ModelResolutionError } from "../model/errors";
52
+
53
+ /** A model as a host offers it; `id` is what `languageModel` receives back as `{ preset: id }`. Never carries a key. */
54
+ export interface ModelProfile {
55
+ id: string;
56
+ label: string;
57
+ endpoint: string;
58
+ model: string;
59
+ source: "preset" | "user";
60
+ }
61
+
62
+ /** The model a request runs on: a preset (registry selector) by id, or an OpenAI-compatible endpoint with its own key. */
63
+ export type ModelRef = { preset: string } | { endpoint: string; model: string; apiKey: string };
64
+
65
+ /** The provider {@link createModelProvider} returns. */
66
+ export interface ModelProvider {
67
+ /** The language model of `ref`; `null` when the registry does not own it. */
68
+ languageModel(ref: ModelRef): Promise<LanguageModelObject | null>;
69
+ /** The registry's declared and discovered models as profiles. */
70
+ listModels(): Promise<ModelProfile[]>;
71
+ }
72
+
73
+ /** Policy and credential store for a model call; see `CredentialPolicy`. */
74
+ export interface ModelPolicyOptions {
75
+ /** Which credential references may be resolved; refused entries are not offered. */
76
+ policy?: CredentialPolicy;
77
+ /** Local encrypted store for `keyring:` references; else `FORGE_CREDENTIAL_STORE` of the env. */
78
+ path?: string;
79
+ /** 64-char hex key of the local store; else `FORGE_CREDENTIAL_KEY` of the env. */
80
+ key?: string;
81
+ }
82
+
83
+ /** The entries the AI SDK constructor can serve (surface and endpoint), by id; policy-refused ones left out. */
84
+ function servedEntries(
85
+ registry: AIProviderRegistryView,
86
+ policy: CredentialPolicy | undefined,
87
+ ): Map<string, AIProviderLlmEntry> {
88
+ const entries = new Map<string, AIProviderLlmEntry>();
89
+ for (const [id, entry] of entriesOfKind(registry, "llm")) {
90
+ const surface = entry.surface ?? "openai-completions";
91
+ if (!(AI_SDK_SURFACES as readonly string[]).includes(surface)) continue;
92
+ if (!resolveEntryBaseUrl(entry)) continue;
93
+ if (entryPolicyRefusal(entry, policy)) continue;
94
+ entries.set(id, entry);
95
+ }
96
+ return entries;
97
+ }
98
+
99
+ /** One registry model as a profile: its selector `id`, shown as `<providerId> · <name>`. */
100
+ function registryProfile(
101
+ providerId: string,
102
+ entry: AIProviderLlmEntry,
103
+ model: { id: string; name: string; providerModelId: string },
104
+ ): ModelProfile {
105
+ return {
106
+ id: model.id,
107
+ label: `${providerId} · ${model.name}`,
108
+ endpoint: resolveEntryBaseUrl(entry)!,
109
+ model: model.providerModelId,
110
+ source: "preset",
111
+ };
112
+ }
113
+
114
+ /** The declared models of the served `entries` as profiles, grouped by provider id in registry order. */
115
+ function declaredProfiles(
116
+ registry: AIProviderRegistryView,
117
+ entries: Map<string, AIProviderLlmEntry>,
118
+ ): Map<string, ModelProfile[]> {
119
+ const byProvider = new Map<string, ModelProfile[]>();
120
+ for (const model of listAIProviderModels(registry)) {
121
+ const entry = entries.get(model.providerId);
122
+ if (!entry) continue;
123
+ const profile = registryProfile(model.providerId, entry, {
124
+ id: model.id,
125
+ name: model.modelId,
126
+ providerModelId: model.providerModelId,
127
+ });
128
+ const own = byProvider.get(model.providerId);
129
+ if (own) own.push(profile);
130
+ else byProvider.set(model.providerId, [profile]);
131
+ }
132
+ return byProvider;
133
+ }
134
+
135
+ /**
136
+ * The models the registry declares (`model_map` / `capabilities`) as model
137
+ * profiles, in registry order — no network. With a policy, entries it
138
+ * refuses are left out. {@link createModelProvider} lists these plus the
139
+ * models discovered live.
140
+ */
141
+ export function declaredModelProfiles(
142
+ registry: AIProviderRegistryView,
143
+ options: ModelPolicyOptions = {},
144
+ ): ModelProfile[] {
145
+ return [...declaredProfiles(registry, servedEntries(registry, options.policy)).values()].flat();
146
+ }
147
+
148
+ /**
149
+ * Declared and discovered models as profiles, in registry order, one
150
+ * profile per selector.
151
+ *
152
+ * An entry's models list is asked when the entry declares `discovery` (and not
153
+ * `type: none`) or declares no models; it is asked with the credential the
154
+ * policy allows, exactly as inference resolves it. A failed or unsupported
155
+ * list leaves that entry with its declared models. A discovered id that a
156
+ * declared model already names (as selector or as mapped provider id) is
157
+ * dropped: the declared identity wins.
158
+ */
159
+ export async function listModelProfiles(
160
+ registry: AIProviderRegistryView,
161
+ env: Record<string, string | undefined>,
162
+ options: ModelPolicyOptions = {},
163
+ ): Promise<ModelProfile[]> {
164
+ const entries = servedEntries(registry, options.policy);
165
+ const declared = declaredProfiles(registry, entries);
166
+ const listed = await Promise.all(
167
+ [...entries].map(async ([providerId, entry]): Promise<ModelProfile[]> => {
168
+ const own = declared.get(providerId) ?? [];
169
+ const listable =
170
+ entry.discovery !== undefined ? entry.discovery.type !== "none" : own.length === 0;
171
+ if (!listable) return own;
172
+ const result = await discoverProviderModels(
173
+ providerId,
174
+ registry,
175
+ credentialStoreOptions(env, options),
176
+ );
177
+ if (result.status !== "live") return own;
178
+ const taken = new Set(
179
+ own.flatMap((profile) => [profile.id, `${providerId}/${profile.model}`]),
180
+ );
181
+ const discovered = result.models.flatMap((model) => {
182
+ const id = `${providerId}/${model.id}`;
183
+ if (taken.has(id)) return [];
184
+ taken.add(id);
185
+ return [
186
+ registryProfile(providerId, entry, { id, name: model.name, providerModelId: model.id }),
187
+ ];
188
+ });
189
+ return [...own, ...discovered];
190
+ }),
191
+ );
192
+ return listed.flat();
193
+ }
194
+
195
+ /**
196
+ * The language model of selector `ref.preset`, or `null` when the registry does
197
+ * not own it: a browser-defined `{ endpoint, model, apiKey }` reference, or a
198
+ * preset id whose leading segment names no registry entry, is the host's own
199
+ * to resolve.
200
+ *
201
+ * Delegates to `resolveRegistryLanguageModel` (`@skaile/forge-settings/ai-sdk`),
202
+ * so the selector's model id may be declared or discovered: an id the entry's
203
+ * `model_map` maps is sent as the mapped id, any other is sent verbatim.
204
+ *
205
+ * @throws {ModelResolutionError} for an owned selector: `unknown_selector` when
206
+ * the entry is disabled or not an `llm` entry, `policy_refused` when the
207
+ * policy refuses it (checked before its credential is read), `unresolved`
208
+ * when its credential, endpoint or SDK does not resolve. No message carries a
209
+ * secret.
210
+ */
211
+ export async function registryLanguageModel(
212
+ registry: AIProviderRegistryView,
213
+ env: Record<string, string | undefined>,
214
+ ref: ModelRef,
215
+ options: ModelPolicyOptions = {},
216
+ ): Promise<LanguageModelObject | null> {
217
+ if (!("preset" in ref)) return null;
218
+ const merged = asMergedRegistry(registry);
219
+ const head = selectorProviderId(ref.preset);
220
+ if (head === undefined || merged.providers[head] === undefined) return null;
221
+ const resolved = await resolveRegistryLanguageModel(ref.preset, {
222
+ registry: merged,
223
+ ...credentialStoreOptions(env, options),
224
+ });
225
+ return resolved.model;
226
+ }
227
+
228
+ /** Options for {@link createModelProvider}. */
229
+ export interface ModelProviderOptions extends ModelPolicyOptions {
230
+ /** The registry to serve, read on every call. */
231
+ registry: () => AIProviderRegistryView | Promise<AIProviderRegistryView>;
232
+ /** The environment `env:` references resolve against, read on every call. */
233
+ env: () => Record<string, string | undefined> | Promise<Record<string, string | undefined>>;
234
+ /** Turn a typed failure into the host's own error (e.g. its HTTP error). Default: rethrow as is. */
235
+ onFailure?: (error: ModelResolutionError) => Error;
236
+ }
237
+
238
+ /** A model provider over the host's registry, for a host's model port (e.g. the editor's `models`). */
239
+ export function createModelProvider(options: ModelProviderOptions): ModelProvider {
240
+ return {
241
+ async languageModel(ref) {
242
+ try {
243
+ return await registryLanguageModel(
244
+ await options.registry(),
245
+ await options.env(),
246
+ ref,
247
+ options,
248
+ );
249
+ } catch (error) {
250
+ if (error instanceof ModelResolutionError && options.onFailure)
251
+ throw options.onFailure(error);
252
+ throw error;
253
+ }
254
+ },
255
+ async listModels() {
256
+ return listModelProfiles(await options.registry(), await options.env(), options);
257
+ },
258
+ };
259
+ }