@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 +14 -0
- package/README.md +10 -7
- package/dist/module.json +1 -1
- package/dist/runtime/editor/index.d.ts +18 -92
- package/dist/runtime/editor/index.js +9 -112
- package/dist/runtime/model-provider/index.d.ts +118 -0
- package/dist/runtime/model-provider/index.js +114 -0
- package/package.json +6 -7
- package/src/runtime/editor/index.ts +25 -234
- package/src/runtime/model-provider/index.ts +259 -0
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
|
-
| `/
|
|
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 `/
|
|
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
|
-
##
|
|
54
|
+
## Model provider and the AI SDK
|
|
54
55
|
|
|
55
56
|
```ts
|
|
56
|
-
// server/utils/
|
|
57
|
-
import {
|
|
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 =
|
|
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
|
-
|
|
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,97 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `@skaile/forge-settings/editor`
|
|
3
|
-
*
|
|
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
|
-
*
|
|
6
|
-
*
|
|
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
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
2
|
+
createModelProvider,
|
|
3
|
+
declaredModelProfiles,
|
|
4
|
+
listModelProfiles,
|
|
5
|
+
registryLanguageModel
|
|
6
|
+
} from "../model-provider/index.js";
|
|
13
7
|
export { ModelResolutionError } from "../model/errors.js";
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
|
4
|
-
"description": "Provider and settings operations for Skaile forge apps: provider registry layers, credential store, env layers, model discovery, the providers panel, and
|
|
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`
|
|
3
|
-
*
|
|
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
|
-
*
|
|
6
|
-
*
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
+
}
|