@skaile/forge-settings 1.3.3 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +40 -0
- package/README.md +52 -9
- package/dist/module.json +1 -1
- package/dist/runtime/ai-sdk/index.d.ts +33 -26
- package/dist/runtime/ai-sdk/index.js +34 -16
- package/dist/runtime/components/ForgeProvidersPanel.d.vue.ts +13 -0
- package/dist/runtime/components/ForgeProvidersPanel.vue +461 -78
- package/dist/runtime/components/ForgeProvidersPanel.vue.d.ts +13 -0
- package/dist/runtime/editor/index.d.ts +55 -16
- package/dist/runtime/editor/index.js +82 -40
- package/dist/runtime/model/selector.d.ts +16 -0
- package/dist/runtime/model/selector.js +12 -0
- package/dist/runtime/server/credential-store.d.ts +16 -4
- package/dist/runtime/server/credential-store.js +3 -4
- package/dist/runtime/server/env-layers.d.ts +1 -1
- package/dist/runtime/server/env-layers.js +3 -3
- package/dist/runtime/server/index.d.ts +9 -8
- package/dist/runtime/server/index.js +3 -3
- package/dist/runtime/server/provider-discovery.d.ts +28 -8
- package/dist/runtime/server/provider-discovery.js +57 -33
- package/dist/runtime/server/provider-layers.d.ts +1 -1
- package/dist/runtime/server/provider-layers.js +3 -5
- package/dist/runtime/server/provider-service.d.ts +26 -2
- package/dist/runtime/server/provider-service.js +16 -1
- package/dist/runtime/server/provider-write.d.ts +26 -10
- package/dist/runtime/server/provider-write.js +75 -7
- package/dist/runtime/server/voice-provider.d.ts +61 -0
- package/dist/runtime/server/voice-provider.js +31 -0
- package/dist/runtime/shared/data-zone.d.ts +68 -0
- package/dist/runtime/shared/data-zone.js +79 -0
- package/dist/runtime/shared/harness-providers.d.ts +42 -3
- package/dist/runtime/shared/harness-providers.js +31 -8
- package/dist/runtime/shared/harnesses.d.ts +7 -0
- package/dist/runtime/shared/harnesses.js +18 -0
- package/dist/runtime/shared/provider-accounts.d.ts +215 -0
- package/dist/runtime/shared/provider-accounts.js +255 -0
- package/dist/runtime/shared/provider-registry.d.ts +19 -182
- package/dist/runtime/shared/provider-registry.js +25 -65
- package/dist/runtime/vue/index.d.ts +2 -0
- package/dist/runtime/vue/index.js +2 -0
- package/dist/runtime/vue/providers-panel-model.d.ts +98 -5
- package/dist/runtime/vue/providers-panel-model.js +13 -1
- package/dist/runtime/vue/useProvidersPanel.d.ts +66 -5
- package/dist/runtime/vue/useProvidersPanel.js +417 -29
- package/dist/runtime/{server → workspaces}/harness-settings.d.ts +51 -13
- package/dist/runtime/{server → workspaces}/harness-settings.js +93 -9
- package/dist/runtime/workspaces/index.d.ts +16 -0
- package/dist/runtime/workspaces/index.js +5 -0
- package/dist/runtime/workspaces/language-model.d.ts +45 -0
- package/dist/runtime/workspaces/language-model.js +26 -0
- package/dist/runtime/workspaces/model-endpoint.d.ts +38 -0
- package/dist/runtime/workspaces/model-endpoint.js +54 -0
- package/dist/runtime/workspaces/provider-settings.d.ts +35 -0
- package/dist/runtime/{server → workspaces}/provider-settings.js +0 -31
- package/dist/runtime/{server → workspaces}/settings-defaults.d.ts +1 -1
- package/dist/runtime/{shared → workspaces}/settings-types.d.ts +11 -4
- package/package.json +11 -2
- package/src/runtime/ai-sdk/index.ts +88 -47
- package/src/runtime/components/ForgeProvidersPanel.vue +477 -78
- package/src/runtime/editor/index.ts +158 -63
- package/src/runtime/model/selector.ts +31 -0
- package/src/runtime/server/credential-store.ts +20 -10
- package/src/runtime/server/env-layers.ts +4 -4
- package/src/runtime/server/index.ts +9 -8
- package/src/runtime/server/provider-discovery.ts +114 -48
- package/src/runtime/server/provider-layers.ts +6 -6
- package/src/runtime/server/provider-service.ts +53 -1
- package/src/runtime/server/provider-write.ts +132 -18
- package/src/runtime/server/voice-provider.ts +110 -0
- package/src/runtime/shared/data-zone.ts +179 -0
- package/src/runtime/shared/harness-providers.ts +78 -7
- package/src/runtime/shared/harnesses.ts +25 -0
- package/src/runtime/shared/provider-accounts.ts +526 -0
- package/src/runtime/shared/provider-registry.ts +82 -256
- package/src/runtime/vue/index.ts +2 -0
- package/src/runtime/vue/providers-panel-model.ts +123 -5
- package/src/runtime/vue/useProvidersPanel.ts +589 -37
- package/src/runtime/{server → workspaces}/harness-settings.ts +166 -22
- package/src/runtime/workspaces/index.ts +17 -0
- package/src/runtime/workspaces/language-model.ts +82 -0
- package/src/runtime/workspaces/model-endpoint.ts +100 -0
- package/src/runtime/workspaces/provider-settings.ts +96 -0
- package/src/runtime/{server → workspaces}/settings-defaults.ts +1 -1
- package/src/runtime/{shared → workspaces}/settings-types.ts +11 -4
- package/dist/runtime/server/provider-settings.d.ts +0 -78
- package/src/runtime/server/provider-settings.ts +0 -192
- /package/dist/runtime/{server → workspaces}/settings-defaults.js +0 -0
- /package/dist/runtime/{shared → workspaces}/settings-types.js +0 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AIProviderDataZoneRule } from "../shared/provider-registry.js";
|
|
1
2
|
import type { ProvidersPanelClient } from "../vue/providers-client.js";
|
|
2
3
|
import { type ProvidersPanelLayer } from "../vue/useProvidersPanel.js";
|
|
3
4
|
type __VLS_Props = {
|
|
@@ -22,12 +23,24 @@ type __VLS_Props = {
|
|
|
22
23
|
readOnly?: boolean;
|
|
23
24
|
/** The line above the list. Absent: the default text for `layer`; a string replaces it; `false` hides it. */
|
|
24
25
|
note?: string | false;
|
|
26
|
+
/**
|
|
27
|
+
* Expert mode: the sign-in step also offers the raw reference form
|
|
28
|
+
* (`env:` / `keyring:` / `cmd:` / `platform:`), and expert-only
|
|
29
|
+
* parameters (`api_version`) become editable.
|
|
30
|
+
*/
|
|
31
|
+
expert?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* The host's data-zone rule: rows outside it show a muted note naming the
|
|
34
|
+
* rule (`provider-blocked-<id>`). Absent or `any`: none.
|
|
35
|
+
*/
|
|
36
|
+
dataZoneRule?: AIProviderDataZoneRule;
|
|
25
37
|
};
|
|
26
38
|
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
|
|
27
39
|
changed: () => any;
|
|
28
40
|
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
29
41
|
onChanged?: (() => any) | undefined;
|
|
30
42
|
}>, {
|
|
43
|
+
expert: boolean;
|
|
31
44
|
environmentEnabled: boolean;
|
|
32
45
|
note: string | false;
|
|
33
46
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
@@ -4,11 +4,24 @@
|
|
|
4
4
|
*
|
|
5
5
|
* `@skaile/forge-editor`'s AI routes ask the host's `ModelProvider`
|
|
6
6
|
* (`listModels` / `languageModel`) before their own runtime-config presets.
|
|
7
|
-
* This adapter answers from the host's registry
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
7
|
+
* This adapter answers from the host's registry and is a thin layer over
|
|
8
|
+
* `@skaile/forge-settings/ai-sdk`: a profile's id is the registry selector
|
|
9
|
+
* `<providerId>/<modelId>`, which `languageModel` receives back as
|
|
10
|
+
* `{ preset: id }` and resolves through the same constructor, credential
|
|
11
|
+
* resolver and policy as every other registry model. Every `llm` entry whose
|
|
12
|
+
* surface the AI SDK constructor speaks (`openai-completions`,
|
|
13
|
+
* `openai-responses`, `anthropic-messages`) and that has an endpoint is
|
|
14
|
+
* offered.
|
|
15
|
+
*
|
|
16
|
+
* Listing combines what the registry declares (`model_map` / `capabilities`)
|
|
17
|
+
* with what an entry's own models list reports, for entries that declare
|
|
18
|
+
* `discovery` or declare no models at all. Declared models work without a
|
|
19
|
+
* models list; a declared identity wins over a discovered one.
|
|
20
|
+
*
|
|
21
|
+
* The registry is authoritative for the selectors it owns: an empty registry
|
|
22
|
+
* lists `[]`, and a `<providerId>/…` selector naming a registry entry that is
|
|
23
|
+
* disabled, refused or unresolvable throws instead of falling through to the
|
|
24
|
+
* editor's presets. Only a selector the registry does not own answers `null`.
|
|
12
25
|
*
|
|
13
26
|
* The registry and the credential environment are supplied by the host on every
|
|
14
27
|
* call, so provider edits apply without a restart and nothing reads the ambient
|
|
@@ -16,35 +29,61 @@
|
|
|
16
29
|
* them to HTTP in `onFailure`.
|
|
17
30
|
*
|
|
18
31
|
* `@skaile/forge-editor` is an optional peer of this entry only, for its
|
|
19
|
-
* `ModelProvider` contract (types).
|
|
32
|
+
* `ModelProvider` contract (types).
|
|
20
33
|
*/
|
|
21
34
|
import type { ModelProfile, ModelRef } from "@skaile/forge-editor/ai";
|
|
22
35
|
import type { ModelProvider } from "@skaile/forge-editor/server";
|
|
36
|
+
import { type LanguageModelObject } from "@skaile/provider-registry/ai-sdk";
|
|
23
37
|
import { ModelResolutionError } from "../model/errors.js";
|
|
24
38
|
import { type CredentialPolicy } from "../server/credential-policy.js";
|
|
25
39
|
import { type AIProviderRegistryView } from "../shared/provider-registry.js";
|
|
26
|
-
export { ModelResolutionError } from "../model/errors.js";
|
|
27
40
|
export type { ModelResolutionCode } from "../model/errors.js";
|
|
28
|
-
|
|
41
|
+
export { ModelResolutionError } from "../model/errors.js";
|
|
42
|
+
/** Policy and credential store for an editor model call; see `CredentialPolicy`. */
|
|
29
43
|
export interface EditorModelPolicyOptions {
|
|
30
44
|
/** Which credential references may be resolved; refused entries are not offered. */
|
|
31
45
|
policy?: CredentialPolicy;
|
|
46
|
+
/** Local encrypted store for `keyring:` references; else `FORGE_CREDENTIAL_STORE` of the env. */
|
|
47
|
+
path?: string;
|
|
48
|
+
/** 64-char hex key of the local store; else `FORGE_CREDENTIAL_KEY` of the env. */
|
|
49
|
+
key?: string;
|
|
32
50
|
}
|
|
33
51
|
/**
|
|
34
|
-
* The
|
|
35
|
-
* order. With a policy, entries it
|
|
52
|
+
* The models the registry declares (`model_map` / `capabilities`) as editor
|
|
53
|
+
* model profiles, in registry order — no network. With a policy, entries it
|
|
54
|
+
* refuses are left out. {@link createEditorModelProvider} lists these plus the
|
|
55
|
+
* models discovered live.
|
|
36
56
|
*/
|
|
37
57
|
export declare function editorModelProfiles(registry: AIProviderRegistryView, options?: EditorModelPolicyOptions): ModelProfile[];
|
|
58
|
+
/**
|
|
59
|
+
* Declared and discovered models as editor profiles, in registry order, one
|
|
60
|
+
* profile per selector.
|
|
61
|
+
*
|
|
62
|
+
* An entry's models list is asked when the entry declares `discovery` (and not
|
|
63
|
+
* `type: none`) or declares no models; it is asked with the credential the
|
|
64
|
+
* policy allows, exactly as inference resolves it. A failed or unsupported
|
|
65
|
+
* list leaves that entry with its declared models. A discovered id that a
|
|
66
|
+
* declared model already names (as selector or as mapped provider id) is
|
|
67
|
+
* dropped: the declared identity wins.
|
|
68
|
+
*/
|
|
69
|
+
export declare function listEditorModelProfiles(registry: AIProviderRegistryView, env: Record<string, string | undefined>, options?: EditorModelPolicyOptions): Promise<ModelProfile[]>;
|
|
38
70
|
/**
|
|
39
71
|
* The language model of selector `ref.preset`, or `null` when the registry does
|
|
40
|
-
* not
|
|
41
|
-
* editor's own
|
|
72
|
+
* not own it: a browser-defined `{ endpoint, model, apiKey }` reference, or a
|
|
73
|
+
* preset id whose leading segment names no registry entry, is the editor's own
|
|
74
|
+
* to resolve.
|
|
75
|
+
*
|
|
76
|
+
* Delegates to `resolveRegistryLanguageModel` (`@skaile/forge-settings/ai-sdk`),
|
|
77
|
+
* so the selector's model id may be declared or discovered: an id the entry's
|
|
78
|
+
* `model_map` maps is sent as the mapped id, any other is sent verbatim.
|
|
42
79
|
*
|
|
43
|
-
* @throws {ModelResolutionError}
|
|
44
|
-
* entry
|
|
45
|
-
*
|
|
80
|
+
* @throws {ModelResolutionError} for an owned selector: `unknown_selector` when
|
|
81
|
+
* the entry is disabled or not an `llm` entry, `policy_refused` when the
|
|
82
|
+
* policy refuses it (checked before its credential is read), `unresolved`
|
|
83
|
+
* when its credential, endpoint or SDK does not resolve. No message carries a
|
|
84
|
+
* secret.
|
|
46
85
|
*/
|
|
47
|
-
export declare function editorLanguageModel(registry: AIProviderRegistryView, env: Record<string, string | undefined>, ref: ModelRef, options?: EditorModelPolicyOptions):
|
|
86
|
+
export declare function editorLanguageModel(registry: AIProviderRegistryView, env: Record<string, string | undefined>, ref: ModelRef, options?: EditorModelPolicyOptions): Promise<LanguageModelObject | null>;
|
|
48
87
|
/** Options for {@link createEditorModelProvider}. */
|
|
49
88
|
export interface EditorModelProviderOptions extends EditorModelPolicyOptions {
|
|
50
89
|
/** The registry to serve, read on every call. */
|
|
@@ -1,63 +1,106 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { resolveEntryBaseUrl } from "@skaile/provider-registry";
|
|
2
|
+
import { AI_SDK_SURFACES } from "@skaile/provider-registry/ai-sdk";
|
|
3
|
+
import { resolveRegistryLanguageModel } from "../ai-sdk/index.js";
|
|
4
|
+
import { ModelResolutionError } from "../model/errors.js";
|
|
5
|
+
import { credentialStoreOptions, selectorProviderId } from "../model/selector.js";
|
|
3
6
|
import { entryPolicyRefusal } from "../server/credential-policy.js";
|
|
4
|
-
import {
|
|
7
|
+
import { discoverProviderModels } from "../server/provider-discovery.js";
|
|
5
8
|
import { listAIProviderModels } from "../server/provider-service.js";
|
|
6
9
|
import {
|
|
10
|
+
asMergedRegistry,
|
|
7
11
|
entriesOfKind
|
|
8
12
|
} from "../shared/provider-registry.js";
|
|
9
13
|
export { ModelResolutionError } from "../model/errors.js";
|
|
10
|
-
|
|
11
|
-
function editorEntries(registry) {
|
|
14
|
+
function editorEntries(registry, policy) {
|
|
12
15
|
const entries = /* @__PURE__ */ new Map();
|
|
13
16
|
for (const [id, entry] of entriesOfKind(registry, "llm")) {
|
|
14
|
-
|
|
17
|
+
const surface = entry.surface ?? "openai-completions";
|
|
18
|
+
if (!AI_SDK_SURFACES.includes(surface)) continue;
|
|
19
|
+
if (!resolveEntryBaseUrl(entry)) continue;
|
|
20
|
+
if (entryPolicyRefusal(entry, policy)) continue;
|
|
21
|
+
entries.set(id, entry);
|
|
15
22
|
}
|
|
16
23
|
return entries;
|
|
17
24
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const allowed = (providerId) => {
|
|
21
|
-
const entry = entries.get(providerId);
|
|
22
|
-
return entry !== void 0 && !entryPolicyRefusal(entry, options.policy);
|
|
23
|
-
};
|
|
24
|
-
return listAIProviderModels(registry).filter((model) => allowed(model.providerId)).map((model) => ({
|
|
25
|
+
function registryProfile(providerId, entry, model) {
|
|
26
|
+
return {
|
|
25
27
|
id: model.id,
|
|
26
|
-
label: `${
|
|
27
|
-
endpoint:
|
|
28
|
+
label: `${providerId} \xB7 ${model.name}`,
|
|
29
|
+
endpoint: resolveEntryBaseUrl(entry),
|
|
28
30
|
model: model.providerModelId,
|
|
29
31
|
source: "preset"
|
|
30
|
-
}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function declaredProfiles(registry, entries) {
|
|
35
|
+
const byProvider = /* @__PURE__ */ new Map();
|
|
36
|
+
for (const model of listAIProviderModels(registry)) {
|
|
37
|
+
const entry = entries.get(model.providerId);
|
|
38
|
+
if (!entry) continue;
|
|
39
|
+
const profile = registryProfile(model.providerId, entry, {
|
|
40
|
+
id: model.id,
|
|
41
|
+
name: model.modelId,
|
|
42
|
+
providerModelId: model.providerModelId
|
|
43
|
+
});
|
|
44
|
+
const own = byProvider.get(model.providerId);
|
|
45
|
+
if (own) own.push(profile);
|
|
46
|
+
else byProvider.set(model.providerId, [profile]);
|
|
47
|
+
}
|
|
48
|
+
return byProvider;
|
|
49
|
+
}
|
|
50
|
+
export function editorModelProfiles(registry, options = {}) {
|
|
51
|
+
return [...declaredProfiles(registry, editorEntries(registry, options.policy)).values()].flat();
|
|
31
52
|
}
|
|
32
|
-
export function
|
|
53
|
+
export async function listEditorModelProfiles(registry, env, options = {}) {
|
|
54
|
+
const entries = editorEntries(registry, options.policy);
|
|
55
|
+
const declared = declaredProfiles(registry, entries);
|
|
56
|
+
const listed = await Promise.all(
|
|
57
|
+
[...entries].map(async ([providerId, entry]) => {
|
|
58
|
+
const own = declared.get(providerId) ?? [];
|
|
59
|
+
const listable = entry.discovery !== void 0 ? entry.discovery.type !== "none" : own.length === 0;
|
|
60
|
+
if (!listable) return own;
|
|
61
|
+
const result = await discoverProviderModels(
|
|
62
|
+
providerId,
|
|
63
|
+
registry,
|
|
64
|
+
credentialStoreOptions(env, options)
|
|
65
|
+
);
|
|
66
|
+
if (result.status !== "live") return own;
|
|
67
|
+
const taken = new Set(
|
|
68
|
+
own.flatMap((profile) => [profile.id, `${providerId}/${profile.model}`])
|
|
69
|
+
);
|
|
70
|
+
const discovered = result.models.flatMap((model) => {
|
|
71
|
+
const id = `${providerId}/${model.id}`;
|
|
72
|
+
if (taken.has(id)) return [];
|
|
73
|
+
taken.add(id);
|
|
74
|
+
return [
|
|
75
|
+
registryProfile(providerId, entry, { id, name: model.name, providerModelId: model.id })
|
|
76
|
+
];
|
|
77
|
+
});
|
|
78
|
+
return [...own, ...discovered];
|
|
79
|
+
})
|
|
80
|
+
);
|
|
81
|
+
return listed.flat();
|
|
82
|
+
}
|
|
83
|
+
export async function editorLanguageModel(registry, env, ref, options = {}) {
|
|
33
84
|
if (!("preset" in ref)) return null;
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
if (
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
env,
|
|
40
|
-
...options.policy ? { policy: options.policy } : {}
|
|
85
|
+
const merged = asMergedRegistry(registry);
|
|
86
|
+
const head = selectorProviderId(ref.preset);
|
|
87
|
+
if (head === void 0 || merged.providers[head] === void 0) return null;
|
|
88
|
+
const resolved = await resolveRegistryLanguageModel(ref.preset, {
|
|
89
|
+
registry: merged,
|
|
90
|
+
...credentialStoreOptions(env, options)
|
|
41
91
|
});
|
|
42
|
-
|
|
43
|
-
throw new ModelResolutionError(
|
|
44
|
-
"unresolved",
|
|
45
|
-
`provider '${model.providerId}': ${credential.error}`,
|
|
46
|
-
{ providerId: model.providerId }
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
return createOpenAICompatible({
|
|
50
|
-
name: model.providerId,
|
|
51
|
-
baseURL: entry.base_url,
|
|
52
|
-
...credential.value ? { apiKey: credential.value } : {},
|
|
53
|
-
...entry.headers ? { headers: entry.headers } : {}
|
|
54
|
-
}).chatModel(model.providerModelId);
|
|
92
|
+
return resolved.model;
|
|
55
93
|
}
|
|
56
94
|
export function createEditorModelProvider(options) {
|
|
57
95
|
return {
|
|
58
96
|
async languageModel(ref) {
|
|
59
97
|
try {
|
|
60
|
-
return
|
|
98
|
+
return await editorLanguageModel(
|
|
99
|
+
await options.registry(),
|
|
100
|
+
await options.env(),
|
|
101
|
+
ref,
|
|
102
|
+
options
|
|
103
|
+
);
|
|
61
104
|
} catch (error) {
|
|
62
105
|
if (error instanceof ModelResolutionError && options.onFailure)
|
|
63
106
|
throw options.onFailure(error);
|
|
@@ -65,8 +108,7 @@ export function createEditorModelProvider(options) {
|
|
|
65
108
|
}
|
|
66
109
|
},
|
|
67
110
|
async listModels() {
|
|
68
|
-
|
|
69
|
-
return profiles.length > 0 ? profiles : null;
|
|
111
|
+
return listEditorModelProfiles(await options.registry(), await options.env(), options);
|
|
70
112
|
}
|
|
71
113
|
};
|
|
72
114
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small shared pieces of registry model resolution: the provider a selector
|
|
3
|
+
* names and the credential-store options a resolution carries.
|
|
4
|
+
*/
|
|
5
|
+
import type { CredentialStoreOptions } from "../server/credential-store.js";
|
|
6
|
+
/**
|
|
7
|
+
* The provider id a `<providerId>/<modelId>` selector names: the segment before
|
|
8
|
+
* the first `/`. `undefined` for an `@role` selector or a bare model id.
|
|
9
|
+
*/
|
|
10
|
+
export declare function selectorProviderId(selector: string): string | undefined;
|
|
11
|
+
/** The policy and local store a credential resolution uses. */
|
|
12
|
+
export type CredentialAccessOptions = Pick<CredentialStoreOptions, "policy" | "path" | "key">;
|
|
13
|
+
/** `env` plus whichever of `policy`, `path` and `key` are set, without `undefined` keys. */
|
|
14
|
+
export declare function credentialStoreOptions(env: Record<string, string | undefined>, { policy, path, key }: CredentialAccessOptions): CredentialStoreOptions & {
|
|
15
|
+
env: Record<string, string | undefined>;
|
|
16
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function selectorProviderId(selector) {
|
|
2
|
+
const slash = selector.indexOf("/");
|
|
3
|
+
return !selector.startsWith("@") && slash > 0 ? selector.slice(0, slash) : void 0;
|
|
4
|
+
}
|
|
5
|
+
export function credentialStoreOptions(env, { policy, path, key }) {
|
|
6
|
+
return {
|
|
7
|
+
env,
|
|
8
|
+
...policy ? { policy } : {},
|
|
9
|
+
...path ? { path } : {},
|
|
10
|
+
...key ? { key } : {}
|
|
11
|
+
};
|
|
12
|
+
}
|
|
@@ -24,6 +24,10 @@
|
|
|
24
24
|
*
|
|
25
25
|
* Blob format: `enc:<base64(iv[16] + tag[16] + ciphertext)>`.
|
|
26
26
|
*
|
|
27
|
+
* Store path: no implicit home. The caller passes `path` or sets
|
|
28
|
+
* `FORGE_CREDENTIAL_STORE` in the environment it hands over; see
|
|
29
|
+
* {@link credentialStorePath}.
|
|
30
|
+
*
|
|
27
31
|
* Key format: `FORGE_CREDENTIAL_KEY` must be a 64-char hex string (32 bytes).
|
|
28
32
|
*/
|
|
29
33
|
import type { AIProviderEntry, AIProviderRegistryView } from "../shared/provider-registry.js";
|
|
@@ -72,7 +76,7 @@ export declare function decryptCredential(ciphertext: string, hexKey: string, re
|
|
|
72
76
|
export interface CredentialStoreOptions {
|
|
73
77
|
/** 64-char hex key; defaults to `FORGE_CREDENTIAL_KEY` of the environment. */
|
|
74
78
|
key?: string;
|
|
75
|
-
/** Store file; defaults to `FORGE_CREDENTIAL_STORE
|
|
79
|
+
/** Store file; defaults to the environment's `FORGE_CREDENTIAL_STORE`. Required for keyring operations. */
|
|
76
80
|
path?: string;
|
|
77
81
|
/**
|
|
78
82
|
* Environment `env:` references (and the key and store path variables) read
|
|
@@ -87,11 +91,19 @@ export interface CredentialStoreOptions {
|
|
|
87
91
|
*/
|
|
88
92
|
policy?: CredentialPolicy;
|
|
89
93
|
}
|
|
94
|
+
/** What a keyring operation throws when neither a path nor `FORGE_CREDENTIAL_STORE` names the store. */
|
|
95
|
+
export declare const CREDENTIAL_STORE_PATH_REQUIRED: "FORGE_CREDENTIAL_STORE or credential store path is required";
|
|
90
96
|
/**
|
|
91
|
-
* Path of the local keyring file.
|
|
97
|
+
* Path of the local keyring file: `options.path`, else the selected
|
|
98
|
+
* environment's `FORGE_CREDENTIAL_STORE`.
|
|
99
|
+
*
|
|
100
|
+
* There is no implicit home: the host decides where its secrets live. A
|
|
101
|
+
* Workspaces host passes `<skaile home>/forge-credentials.json` (see
|
|
102
|
+
* `@skaile/forge-settings/workspaces`). Only keyring operations ask for the
|
|
103
|
+
* path, so env-only and keyless providers need none.
|
|
92
104
|
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
105
|
+
* @throws Error `FORGE_CREDENTIAL_STORE or credential store path is required`
|
|
106
|
+
* when neither is given.
|
|
95
107
|
*/
|
|
96
108
|
export declare function credentialStorePath(options?: CredentialStoreOptions): string;
|
|
97
109
|
/**
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { dirname
|
|
4
|
-
import { globalSettingsPath } from "@skaile/workspaces/sdk/core";
|
|
3
|
+
import { dirname } from "node:path";
|
|
5
4
|
import { isEntryEnabled, parseCredentialRef, registryEntries } from "../shared/provider-registry.js";
|
|
6
5
|
import {
|
|
7
6
|
credentialRefRefusal,
|
|
8
7
|
entryPolicyRefusal
|
|
9
8
|
} from "./credential-policy.js";
|
|
10
9
|
export const CREDENTIAL_PREFIX = "enc:";
|
|
11
|
-
const STORE_FILENAME = "forge-credentials.json";
|
|
12
10
|
const ALGORITHM = "aes-256-gcm";
|
|
13
11
|
const IV_LENGTH = 16;
|
|
14
12
|
const TAG_LENGTH = 16;
|
|
@@ -85,11 +83,12 @@ export function decryptCredential(ciphertext, hexKey, ref) {
|
|
|
85
83
|
function envOf(options) {
|
|
86
84
|
return options.env ?? (options.policy ? {} : process.env);
|
|
87
85
|
}
|
|
86
|
+
export const CREDENTIAL_STORE_PATH_REQUIRED = "FORGE_CREDENTIAL_STORE or credential store path is required";
|
|
88
87
|
export function credentialStorePath(options = {}) {
|
|
89
88
|
if (options.path) return options.path;
|
|
90
89
|
const fromEnv = envOf(options).FORGE_CREDENTIAL_STORE;
|
|
91
90
|
if (fromEnv) return fromEnv;
|
|
92
|
-
|
|
91
|
+
throw new Error(CREDENTIAL_STORE_PATH_REQUIRED);
|
|
93
92
|
}
|
|
94
93
|
function requireKey(options) {
|
|
95
94
|
const key = options.key ?? envOf(options).FORGE_CREDENTIAL_KEY;
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* browser-safe `utils/env-vars.ts` and is re-exported here, so client and
|
|
14
14
|
* server refuse the same names from one source.
|
|
15
15
|
*/
|
|
16
|
-
import type { AIProviderRegistryView } from "../shared/provider-registry.js";
|
|
17
16
|
import { type EnvLayerId, type EnvSettingsDocument, type EnvSettingsUpdate } from "../shared/env-vars.js";
|
|
17
|
+
import type { AIProviderRegistryView } from "../shared/provider-registry.js";
|
|
18
18
|
export * from "../shared/env-vars.js";
|
|
19
19
|
/**
|
|
20
20
|
* Parse `.env` text: blank and `#` lines skipped, optional `export ` prefix,
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { chmodSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
|
-
import { AI_PROVIDER_PRESETS } from "@skaile/
|
|
4
|
-
import { isPlainObject } from "../shared/guards.js";
|
|
5
|
-
import { parseCredentialRef, registryEntries } from "../shared/provider-registry.js";
|
|
3
|
+
import { AI_PROVIDER_PRESETS } from "@skaile/provider-registry/catalog";
|
|
6
4
|
import {
|
|
7
5
|
ENV_LAYER_ORDER,
|
|
8
6
|
validateEnvVarName
|
|
9
7
|
} from "../shared/env-vars.js";
|
|
8
|
+
import { isPlainObject } from "../shared/guards.js";
|
|
9
|
+
import { parseCredentialRef, registryEntries } from "../shared/provider-registry.js";
|
|
10
10
|
export * from "../shared/env-vars.js";
|
|
11
11
|
const ASSIGNMENT = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_.-]*)\s*=(.*)$/;
|
|
12
12
|
const NEEDS_QUOTES = /[\s#"'=\\]/;
|
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `@skaile/forge-settings/server` — provider
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* `@skaile/forge-settings/server` — provider operations for a host server: the
|
|
3
|
+
* registry vocabulary and layer editing, the credential store, env layers,
|
|
4
|
+
* model discovery and voice-entry selection.
|
|
5
5
|
*
|
|
6
|
-
* Imports no Vue, no editor, no AI SDK
|
|
6
|
+
* Imports no Vue, no editor, no AI SDK, no agent session code and no Skaile
|
|
7
|
+
* Workspaces: the Workspaces settings document, harness settings and project
|
|
8
|
+
* registry loading live in `@skaile/forge-settings/workspaces`. Storage,
|
|
7
9
|
* authentication and workspace permissions stay with the host.
|
|
8
10
|
*/
|
|
11
|
+
export * from "../shared/data-zone.js";
|
|
9
12
|
export * from "../shared/harness-providers.js";
|
|
10
13
|
export * from "../shared/harnesses.js";
|
|
11
14
|
export * from "../shared/model-roles.js";
|
|
15
|
+
export * from "../shared/provider-accounts.js";
|
|
12
16
|
export * from "../shared/provider-registry.js";
|
|
13
|
-
export type { DomainConfig, SkaileSettings } from "../shared/settings-types.js";
|
|
14
17
|
export * from "./credential-policy.js";
|
|
15
18
|
export * from "./credential-store.js";
|
|
16
19
|
export * from "./env.js";
|
|
17
20
|
export * from "./env-layers.js";
|
|
18
|
-
export * from "./harness-settings.js";
|
|
19
21
|
export * from "./provider-discovery.js";
|
|
20
22
|
export * from "./provider-layers.js";
|
|
21
23
|
export * from "./provider-service.js";
|
|
22
|
-
export * from "./provider-settings.js";
|
|
23
24
|
export * from "./provider-write.js";
|
|
24
|
-
export * from "./
|
|
25
|
+
export * from "./voice-provider.js";
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
+
export * from "../shared/data-zone.js";
|
|
1
2
|
export * from "../shared/harness-providers.js";
|
|
2
3
|
export * from "../shared/harnesses.js";
|
|
3
4
|
export * from "../shared/model-roles.js";
|
|
5
|
+
export * from "../shared/provider-accounts.js";
|
|
4
6
|
export * from "../shared/provider-registry.js";
|
|
5
7
|
export * from "./credential-policy.js";
|
|
6
8
|
export * from "./credential-store.js";
|
|
7
9
|
export * from "./env.js";
|
|
8
10
|
export * from "./env-layers.js";
|
|
9
|
-
export * from "./harness-settings.js";
|
|
10
11
|
export * from "./provider-discovery.js";
|
|
11
12
|
export * from "./provider-layers.js";
|
|
12
13
|
export * from "./provider-service.js";
|
|
13
|
-
export * from "./provider-settings.js";
|
|
14
14
|
export * from "./provider-write.js";
|
|
15
|
-
export * from "./
|
|
15
|
+
export * from "./voice-provider.js";
|
|
@@ -5,11 +5,12 @@
|
|
|
5
5
|
* `model_map`; this module asks the provider's REST API. The two are different
|
|
6
6
|
* facts and the result says which one it is:
|
|
7
7
|
*
|
|
8
|
-
* - `live` — the provider answered; `models` is its list.
|
|
9
|
-
* - `static` — the live request failed or the entry
|
|
10
|
-
* the offline catalog of
|
|
11
|
-
*
|
|
12
|
-
*
|
|
8
|
+
* - `live` — the provider answered; `models` is its list (possibly empty).
|
|
9
|
+
* - `static` — the live request failed or the entry speaks no models list, and
|
|
10
|
+
* `models` is the offline catalog of the entry's own preset. It is a
|
|
11
|
+
* labelled fallback, never a live answer and never an inference destination.
|
|
12
|
+
* - `unsupported` — the entry speaks no models list and has no catalog; a host
|
|
13
|
+
* lets the admin type a model id.
|
|
13
14
|
* - `error` — nothing usable. The message is scrubbed.
|
|
14
15
|
*
|
|
15
16
|
* The error text is built here, never forwarded: a remote response body is
|
|
@@ -55,9 +56,12 @@ export interface DiscoverProviderModelsOptions {
|
|
|
55
56
|
policy?: CredentialPolicy;
|
|
56
57
|
/** Extra strings to remove from error text, besides the secrets this call resolves. */
|
|
57
58
|
redact?: readonly string[];
|
|
58
|
-
/** Local encrypted store for `keyring:` references
|
|
59
|
-
key?: string;
|
|
59
|
+
/** Local encrypted store for `keyring:` references; else `FORGE_CREDENTIAL_STORE` of `env`. */
|
|
60
60
|
path?: string;
|
|
61
|
+
/** 64-char hex key of the local store; else `FORGE_CREDENTIAL_KEY` of `env`. */
|
|
62
|
+
key?: string;
|
|
63
|
+
/** Cancels a live request; the result is then an `error` / `static` one. */
|
|
64
|
+
signal?: AbortSignal;
|
|
61
65
|
}
|
|
62
66
|
/**
|
|
63
67
|
* Remove every occurrence of each secret (verbatim, URL-encoded and
|
|
@@ -67,7 +71,23 @@ export declare function scrubSecrets(text: string, secrets: readonly (string | u
|
|
|
67
71
|
/**
|
|
68
72
|
* Ask one registry entry's provider for its models.
|
|
69
73
|
*
|
|
74
|
+
* Two ways to ask, both with the credential resolved here under the policy:
|
|
75
|
+
*
|
|
76
|
+
* - A catalog-preset entry with a resolved key and no `discovery` block of its
|
|
77
|
+
* own uses the preset's list parser (`fetchProviderModels`), which filters
|
|
78
|
+
* the vendor's list the way that vendor needs.
|
|
79
|
+
* - Every other entry — a custom endpoint, a keyless local server, an entry
|
|
80
|
+
* that declares `discovery` — asks its own `base_url` with its own headers
|
|
81
|
+
* and credential carrier (`fetchEntryModels`), exactly as inference would.
|
|
82
|
+
* No preset and no secret are required: `credential.kind: none` queries
|
|
83
|
+
* without `Authorization`.
|
|
84
|
+
*
|
|
85
|
+
* A successful empty list is `live` and empty. Only a preset entry has a
|
|
86
|
+
* `static` fallback: its own catalog, labelled as such, never another
|
|
87
|
+
* provider and never an inference destination.
|
|
88
|
+
*
|
|
70
89
|
* Never throws for a provider-side failure: that is the `error` / `static`
|
|
71
|
-
* result. A host maps `error` to its own status.
|
|
90
|
+
* result. A host maps `error` to its own status. A refused or unresolvable
|
|
91
|
+
* credential sends no request at all.
|
|
72
92
|
*/
|
|
73
93
|
export declare function discoverProviderModels(providerId: string, registry: AIProviderRegistryView, options: DiscoverProviderModelsOptions): Promise<ModelDiscoveryResult>;
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
import { resolveEntryBaseUrl } from "@skaile/
|
|
2
|
-
import {
|
|
1
|
+
import { resolveEntryBaseUrl } from "@skaile/provider-registry";
|
|
2
|
+
import { entrySurface, getStaticModels } from "@skaile/provider-registry/catalog";
|
|
3
|
+
import {
|
|
4
|
+
fetchEntryModels,
|
|
5
|
+
fetchProviderModels
|
|
6
|
+
} from "@skaile/provider-registry/discovery";
|
|
7
|
+
import { credentialStoreOptions } from "../model/selector.js";
|
|
3
8
|
import { isEntryEnabled, registryEntries } from "../shared/provider-registry.js";
|
|
4
9
|
import { entryPolicyRefusal } from "./credential-policy.js";
|
|
5
10
|
import { resolveEntryCredential } from "./credential-store.js";
|
|
@@ -18,6 +23,32 @@ export function scrubSecrets(text, secrets) {
|
|
|
18
23
|
}
|
|
19
24
|
return out.length > MAX_ERROR_LENGTH ? `${out.slice(0, MAX_ERROR_LENGTH)}\u2026` : out;
|
|
20
25
|
}
|
|
26
|
+
function entryListing(entry) {
|
|
27
|
+
const type = entry.discovery?.type;
|
|
28
|
+
if (type === "none") return { ok: false, reason: "this entry declares no model discovery" };
|
|
29
|
+
if (type === "openai-models-list" || type === "anthropic-models-list") return { ok: true };
|
|
30
|
+
if (type !== void 0) return { ok: false, reason: `discovery type '${type}' is not supported` };
|
|
31
|
+
const surface = entrySurface(entry);
|
|
32
|
+
if (surface === "openai-completions" || surface === "openai-responses" || surface === "anthropic-messages") {
|
|
33
|
+
return { ok: true };
|
|
34
|
+
}
|
|
35
|
+
return { ok: false, reason: `surface '${surface ?? "unknown"}' has no models list` };
|
|
36
|
+
}
|
|
37
|
+
const CANCELLED = {
|
|
38
|
+
ok: false,
|
|
39
|
+
models: [],
|
|
40
|
+
error: "models list request was cancelled",
|
|
41
|
+
source: "api"
|
|
42
|
+
};
|
|
43
|
+
function untilAborted(start, signal) {
|
|
44
|
+
if (!signal) return start();
|
|
45
|
+
if (signal.aborted) return Promise.resolve(CANCELLED);
|
|
46
|
+
return new Promise((resolve, reject) => {
|
|
47
|
+
const onAbort = () => resolve(CANCELLED);
|
|
48
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
49
|
+
start().finally(() => signal.removeEventListener("abort", onAbort)).then(resolve, reject);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
21
52
|
export async function discoverProviderModels(providerId, registry, options) {
|
|
22
53
|
const match = registryEntries(registry).find(([id]) => id === providerId);
|
|
23
54
|
if (!match)
|
|
@@ -29,43 +60,36 @@ export async function discoverProviderModels(providerId, registry, options) {
|
|
|
29
60
|
if (!isEntryEnabled(entry)) return { status: "error", providerId, error: "entry is disabled" };
|
|
30
61
|
const refusal = entryPolicyRefusal(entry, options.policy);
|
|
31
62
|
if (refusal) return { status: "error", providerId, error: refusal };
|
|
32
|
-
const credential = resolveEntryCredential(entry,
|
|
33
|
-
env: options.env,
|
|
34
|
-
...options.policy ? { policy: options.policy } : {},
|
|
35
|
-
...options.key ? { key: options.key } : {},
|
|
36
|
-
...options.path ? { path: options.path } : {}
|
|
37
|
-
});
|
|
63
|
+
const credential = resolveEntryCredential(entry, credentialStoreOptions(options.env, options));
|
|
38
64
|
const secrets = [credential.value, ...options.redact ?? []];
|
|
39
65
|
const scrub = (text) => scrubSecrets(text, secrets);
|
|
40
|
-
if (!entry.preset) {
|
|
41
|
-
return {
|
|
42
|
-
status: "unsupported",
|
|
43
|
-
providerId,
|
|
44
|
-
reason: "this entry names no catalog preset, so its models cannot be listed; enter a model id"
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
66
|
if (credential.error && !credential.value) {
|
|
48
67
|
return { status: "error", providerId, error: scrub(credential.error) };
|
|
49
68
|
}
|
|
50
|
-
const staticList = getStaticModels(entry.preset).map(({ id, name }) => ({ id, name }));
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
69
|
+
const staticList = entry.preset ? getStaticModels(entry.preset).map(({ id, name }) => ({ id, name })) : [];
|
|
70
|
+
let live;
|
|
71
|
+
if (entry.preset && !entry.discovery && credential.value) {
|
|
72
|
+
const baseUrl = resolveEntryBaseUrl(entry);
|
|
73
|
+
const apiKey = credential.value;
|
|
74
|
+
live = await untilAborted(
|
|
75
|
+
() => fetchProviderModels(entry.preset, apiKey, {
|
|
76
|
+
...baseUrl ? { baseUrl } : {},
|
|
77
|
+
env: options.env
|
|
78
|
+
}),
|
|
79
|
+
options.signal
|
|
80
|
+
);
|
|
81
|
+
} else {
|
|
82
|
+
const listing = entryListing(entry);
|
|
83
|
+
if (!listing.ok) {
|
|
84
|
+
return staticList.length > 0 ? { status: "static", providerId, models: staticList, reason: listing.reason } : { status: "unsupported", providerId, reason: `${listing.reason}; enter a model id` };
|
|
85
|
+
}
|
|
86
|
+
live = await fetchEntryModels(entry, {
|
|
87
|
+
env: {},
|
|
88
|
+
...credential.value !== void 0 ? { secret: credential.value } : {},
|
|
89
|
+
...options.signal ? { signal: options.signal } : {}
|
|
90
|
+
});
|
|
62
91
|
}
|
|
63
|
-
|
|
64
|
-
const live = await fetchProviderModels(entry.preset, credential.value, {
|
|
65
|
-
...baseUrl ? { baseUrl } : {},
|
|
66
|
-
env: options.env
|
|
67
|
-
});
|
|
68
|
-
if (live.ok && live.models.length > 0) {
|
|
92
|
+
if (live.ok) {
|
|
69
93
|
return {
|
|
70
94
|
status: "live",
|
|
71
95
|
providerId,
|