@skaile/forge-settings 1.4.0 → 2.0.1
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 +31 -0
- package/README.md +34 -11
- 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 +2 -2
- package/dist/runtime/components/ForgeProvidersPanel.vue.d.ts +2 -2
- 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 +7 -8
- package/dist/runtime/server/index.js +1 -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 +2 -2
- package/dist/runtime/server/provider-write.d.ts +1 -1
- package/dist/runtime/server/provider-write.js +3 -3
- 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 +28 -26
- package/dist/runtime/shared/data-zone.js +4 -4
- package/dist/runtime/shared/harness-providers.d.ts +7 -7
- package/dist/runtime/shared/provider-accounts.d.ts +1 -1
- package/dist/runtime/shared/provider-registry.d.ts +19 -220
- package/dist/runtime/shared/provider-registry.js +25 -81
- package/dist/runtime/vue/providers-panel-model.d.ts +4 -4
- package/dist/runtime/{server → workspaces}/harness-settings.d.ts +3 -3
- 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 +4 -4
- package/package.json +13 -4
- package/src/runtime/ai-sdk/index.ts +88 -47
- package/src/runtime/components/ForgeProvidersPanel.vue +2 -2
- 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 +7 -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 +2 -2
- package/src/runtime/server/provider-write.ts +3 -3
- package/src/runtime/server/voice-provider.ts +110 -0
- package/src/runtime/shared/data-zone.ts +40 -33
- package/src/runtime/shared/harness-providers.ts +7 -7
- package/src/runtime/shared/provider-accounts.ts +1 -1
- package/src/runtime/shared/provider-registry.ts +82 -314
- package/src/runtime/vue/providers-panel-model.ts +5 -5
- package/src/runtime/vue/useProvidersPanel.ts +5 -2
- package/src/runtime/{server → workspaces}/harness-settings.ts +3 -3
- 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 +4 -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}/harness-settings.js +0 -0
- /package/dist/runtime/{server → workspaces}/settings-defaults.js +0 -0
- /package/dist/runtime/{shared → workspaces}/settings-types.js +0 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { isPlainObject } from "../shared/guards.js";
|
|
4
|
+
export const WORKSPACE_MODEL_ENDPOINT_URL_ENV = "SKAILE_MODEL_ENDPOINT_URL";
|
|
5
|
+
export const WORKSPACE_MODEL_ENDPOINT_TOKEN_ENV = "SKAILE_MODEL_ENDPOINT_TOKEN";
|
|
6
|
+
const PREFIX = "Skaile model endpoint configuration";
|
|
7
|
+
function isHttpUrl(value) {
|
|
8
|
+
try {
|
|
9
|
+
const { protocol } = new URL(value);
|
|
10
|
+
return protocol === "http:" || protocol === "https:";
|
|
11
|
+
} catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export async function readWorkspaceModelEndpoint(options) {
|
|
16
|
+
const envUrl = options.env[WORKSPACE_MODEL_ENDPOINT_URL_ENV]?.trim() ?? "";
|
|
17
|
+
const envToken = options.env[WORKSPACE_MODEL_ENDPOINT_TOKEN_ENV]?.trim() ?? "";
|
|
18
|
+
if (envUrl || envToken) {
|
|
19
|
+
if (!envUrl || !envToken) {
|
|
20
|
+
throw new Error(
|
|
21
|
+
`${PREFIX}: ${WORKSPACE_MODEL_ENDPOINT_URL_ENV} and ${WORKSPACE_MODEL_ENDPOINT_TOKEN_ENV} must be set together`
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
if (!isHttpUrl(envUrl)) {
|
|
25
|
+
throw new Error(`${PREFIX}: ${WORKSPACE_MODEL_ENDPOINT_URL_ENV} is not an http(s) URL`);
|
|
26
|
+
}
|
|
27
|
+
return { url: envUrl, token: envToken };
|
|
28
|
+
}
|
|
29
|
+
if (options.projectDir === void 0) return null;
|
|
30
|
+
const file = join(options.projectDir, ".skaile", "model-endpoint.json");
|
|
31
|
+
let text;
|
|
32
|
+
try {
|
|
33
|
+
text = await readFile(file, "utf8");
|
|
34
|
+
} catch (error) {
|
|
35
|
+
if (error.code === "ENOENT") return null;
|
|
36
|
+
throw new Error(`${PREFIX}: ${file} could not be read`);
|
|
37
|
+
}
|
|
38
|
+
let parsed;
|
|
39
|
+
try {
|
|
40
|
+
parsed = JSON.parse(text);
|
|
41
|
+
} catch {
|
|
42
|
+
throw new Error(`${PREFIX}: ${file} is not valid JSON`);
|
|
43
|
+
}
|
|
44
|
+
const record = isPlainObject(parsed) ? parsed : {};
|
|
45
|
+
const url = typeof record.url === "string" ? record.url.trim() : "";
|
|
46
|
+
const token = typeof record.token === "string" ? record.token.trim() : "";
|
|
47
|
+
if (!url || !token) {
|
|
48
|
+
throw new Error(`${PREFIX}: ${file} needs a non-empty "url" and "token"`);
|
|
49
|
+
}
|
|
50
|
+
if (!isHttpUrl(url)) {
|
|
51
|
+
throw new Error(`${PREFIX}: ${file} "url" is not an http(s) URL`);
|
|
52
|
+
}
|
|
53
|
+
return { url, token };
|
|
54
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The legacy `settings.json` migration of a Workspaces host.
|
|
3
|
+
*
|
|
4
|
+
* A `settings.json` written before the 5.0.0 registry migration holds
|
|
5
|
+
* `provider` + a free-text `model`, and an `apiKeys` map of secrets. On first
|
|
6
|
+
* load the pair collapses into one `"<providerId>/<modelId>"` selector, and the
|
|
7
|
+
* `apiKeys` map is dropped with a warning — a plaintext key is never
|
|
8
|
+
* auto-written anywhere; the user moves it into `.env`.
|
|
9
|
+
*
|
|
10
|
+
* Voice selection over the same settings fields is generic:
|
|
11
|
+
* `resolveVoiceProvider` in `@skaile/forge-settings/server`.
|
|
12
|
+
*/
|
|
13
|
+
/** What {@link migrateLegacyProviderSettings} did to one settings file. */
|
|
14
|
+
export interface LegacySettingsMigration {
|
|
15
|
+
/** The file that was inspected. */
|
|
16
|
+
path: string;
|
|
17
|
+
/** Whether the file was rewritten. */
|
|
18
|
+
changed: boolean;
|
|
19
|
+
/** The selector `provider` + `model` collapsed into, when that happened. */
|
|
20
|
+
model?: string;
|
|
21
|
+
/** Everything dropped or left untouched, and why. Secret-free. */
|
|
22
|
+
warnings: string[];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Migrate one legacy `settings.json` in place. Idempotent: a file with no
|
|
26
|
+
* legacy `provider` and no `apiKeys` is left byte-identical.
|
|
27
|
+
*
|
|
28
|
+
* An `apiKeys` map is removed from the file and never stored anywhere else:
|
|
29
|
+
* the registry holds references only, and a plaintext key has no safe
|
|
30
|
+
* automatic destination. Each non-empty entry is reported through `warnings`
|
|
31
|
+
* (without the key) so the user can move it into `.env`.
|
|
32
|
+
*
|
|
33
|
+
* @param settingsPath - Absolute path to a `settings.json`.
|
|
34
|
+
*/
|
|
35
|
+
export declare function migrateLegacyProviderSettings(settingsPath: string): LegacySettingsMigration;
|
|
@@ -1,9 +1,4 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import {
|
|
3
|
-
entriesOfKind,
|
|
4
|
-
selectVoiceEntryId,
|
|
5
|
-
VOICE_OVERRIDE_PARAM
|
|
6
|
-
} from "../shared/provider-registry.js";
|
|
7
2
|
export function migrateLegacyProviderSettings(settingsPath) {
|
|
8
3
|
const result = {
|
|
9
4
|
path: settingsPath,
|
|
@@ -53,29 +48,3 @@ export function migrateLegacyProviderSettings(settingsPath) {
|
|
|
53
48
|
}
|
|
54
49
|
return result;
|
|
55
50
|
}
|
|
56
|
-
export function resolveVoiceProvider(registry, kind, settings = {}, overrides = {}) {
|
|
57
|
-
const candidates = entriesOfKind(registry, kind);
|
|
58
|
-
if (candidates.length === 0) return void 0;
|
|
59
|
-
const warnings = [];
|
|
60
|
-
const wanted = (kind === "stt" ? settings.sttProvider : settings.ttsProvider)?.trim();
|
|
61
|
-
const ids = candidates.map(([id]) => id);
|
|
62
|
-
const providerId = selectVoiceEntryId(ids, wanted);
|
|
63
|
-
const entry = candidates[ids.indexOf(providerId)][1];
|
|
64
|
-
if (wanted && wanted !== providerId) {
|
|
65
|
-
warnings.push(
|
|
66
|
-
`settings name '${wanted}' for ${kind}, which is not an enabled ${kind} entry \u2014 using '${providerId}' instead`
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
const fallback = wanted !== providerId;
|
|
70
|
-
const settingsOverride = {};
|
|
71
|
-
const override = (kind === "stt" ? settings.sttLanguage : settings.ttsVoice)?.trim();
|
|
72
|
-
if (override) settingsOverride[VOICE_OVERRIDE_PARAM[kind]] = override;
|
|
73
|
-
return {
|
|
74
|
-
providerId,
|
|
75
|
-
entry,
|
|
76
|
-
...entry.base_url ? { baseUrl: entry.base_url } : {},
|
|
77
|
-
params: { ...entry.params, ...settingsOverride, ...overrides },
|
|
78
|
-
fallback,
|
|
79
|
-
warnings
|
|
80
|
-
};
|
|
81
|
-
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Storage stays with the host: this module reads and writes nothing except the
|
|
5
5
|
* one `settings.json` {@link migrateLegacyHarnessSettings} is pointed at.
|
|
6
6
|
*/
|
|
7
|
-
import type { SkaileSettings } from "
|
|
7
|
+
import type { SkaileSettings } from "./settings-types.js";
|
|
8
8
|
/**
|
|
9
9
|
* Merged default settings for forge apps.
|
|
10
10
|
*
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The settings document type forge apps share.
|
|
2
|
+
* The settings document type forge apps on Skaile Workspaces share.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* It extends the Workspaces `settings.json` type, so it lives in the explicit
|
|
5
|
+
* `@skaile/forge-settings/workspaces` entry: the generic entries never see it.
|
|
6
|
+
* A leaf module: it imports types only.
|
|
7
7
|
*/
|
|
8
8
|
import type { SkaileSettings as CoreSettings } from "@skaile/workspaces/sdk/core";
|
|
9
9
|
/** Skill domain enablement configuration for a forge app installation. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skaile/forge-settings",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "Provider and settings operations for Skaile forge apps: provider registry layers, credential store, env layers, model discovery, the providers panel, and editor / AI SDK model adapters.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "bun@1.3.9",
|
|
@@ -26,6 +26,10 @@
|
|
|
26
26
|
"./editor": {
|
|
27
27
|
"types": "./dist/runtime/editor/index.d.ts",
|
|
28
28
|
"import": "./dist/runtime/editor/index.js"
|
|
29
|
+
},
|
|
30
|
+
"./workspaces": {
|
|
31
|
+
"types": "./dist/runtime/workspaces/index.d.ts",
|
|
32
|
+
"import": "./dist/runtime/workspaces/index.js"
|
|
29
33
|
}
|
|
30
34
|
},
|
|
31
35
|
"files": [
|
|
@@ -55,7 +59,7 @@
|
|
|
55
59
|
},
|
|
56
60
|
"dependencies": {
|
|
57
61
|
"@nuxt/kit": "^4.0.0",
|
|
58
|
-
"@skaile/
|
|
62
|
+
"@skaile/provider-registry": "^0.1.0",
|
|
59
63
|
"ofetch": "^1.4.0",
|
|
60
64
|
"yaml": "^2.9.0"
|
|
61
65
|
},
|
|
@@ -64,7 +68,8 @@
|
|
|
64
68
|
"@ai-sdk/openai": "^4.0.0",
|
|
65
69
|
"@ai-sdk/openai-compatible": "^3.0.0",
|
|
66
70
|
"@nuxt/ui": "^4.11.1",
|
|
67
|
-
"@skaile/forge-editor": "^6.0.0",
|
|
71
|
+
"@skaile/forge-editor": "^6.0.0 || ^7.0.0",
|
|
72
|
+
"@skaile/workspaces": "^7.8.0 || ^8.0.0 || ^9.0.0 || ^10.0.0",
|
|
68
73
|
"ai": "^7.0.0",
|
|
69
74
|
"nuxt": "^4.0.0",
|
|
70
75
|
"vue": "^3.5.0"
|
|
@@ -85,6 +90,9 @@
|
|
|
85
90
|
"@skaile/forge-editor": {
|
|
86
91
|
"optional": true
|
|
87
92
|
},
|
|
93
|
+
"@skaile/workspaces": {
|
|
94
|
+
"optional": true
|
|
95
|
+
},
|
|
88
96
|
"ai": {
|
|
89
97
|
"optional": true
|
|
90
98
|
},
|
|
@@ -101,7 +109,8 @@
|
|
|
101
109
|
"@nuxt/module-builder": "^1.0.0",
|
|
102
110
|
"@nuxt/schema": "^4.0.0",
|
|
103
111
|
"@nuxt/ui": "^4.11.1",
|
|
104
|
-
"@skaile/forge-editor": "^
|
|
112
|
+
"@skaile/forge-editor": "^7.0.0",
|
|
113
|
+
"@skaile/workspaces": "^10.0.0",
|
|
105
114
|
"@types/bun": "^1.2.0",
|
|
106
115
|
"@types/node": "^22.0.0",
|
|
107
116
|
"ai": "^7.0.155",
|
|
@@ -1,33 +1,43 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `@skaile/forge-settings/ai-sdk` — the AI SDK language model of a registry
|
|
3
|
-
* selector,
|
|
4
|
-
* credential
|
|
3
|
+
* selector, built by `@skaile/provider-registry/ai-sdk` against one registry
|
|
4
|
+
* snapshot the host passes, with the credential resolved under the host's
|
|
5
|
+
* policy.
|
|
5
6
|
*
|
|
6
|
-
*
|
|
7
|
-
* this module
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* The shared constructor owns endpoint, header and SDK wiring for every
|
|
8
|
+
* surface it supports; this module decides *whether* an entry may be resolved
|
|
9
|
+
* and supplies its credential — through the same policy-checked resolver model
|
|
10
|
+
* discovery uses, so a model that lists is a model that calls with the same
|
|
11
|
+
* key. It is not a service locator: the caller passes the selector
|
|
12
|
+
* (role-to-selector mapping belongs to the host's settings document), the
|
|
13
|
+
* registry and the environment explicitly. Nothing is read from disk here;
|
|
14
|
+
* a Skaile Workspaces host that resolves against a project's registry uses
|
|
15
|
+
* `resolveWorkspaceLanguageModel` from `@skaile/forge-settings/workspaces`.
|
|
11
16
|
*
|
|
12
17
|
* Provider SDKs (`@ai-sdk/anthropic`, `@ai-sdk/openai`,
|
|
13
|
-
* `@ai-sdk/openai-compatible`) are optional peers
|
|
14
|
-
*
|
|
18
|
+
* `@ai-sdk/openai-compatible`) are optional peers: install the one the
|
|
19
|
+
* selected surface needs.
|
|
15
20
|
*/
|
|
16
21
|
|
|
17
|
-
import { type MergedAIProviderRegistry, parseModelRef } from "@skaile/
|
|
18
|
-
import {
|
|
19
|
-
|
|
22
|
+
import { type MergedAIProviderRegistry, parseModelRef } from "@skaile/provider-registry";
|
|
23
|
+
import {
|
|
24
|
+
type EntryCredentialResolver,
|
|
25
|
+
type ResolvedLanguageModel,
|
|
26
|
+
resolveLanguageModel,
|
|
27
|
+
} from "@skaile/provider-registry/ai-sdk";
|
|
28
|
+
import { assertEntryAllowed, ModelResolutionError } from "../model/errors";
|
|
29
|
+
import { credentialStoreOptions, selectorProviderId } from "../model/selector";
|
|
20
30
|
import type { CredentialPolicy } from "../server/credential-policy";
|
|
21
|
-
import {
|
|
31
|
+
import { resolveEntryCredential } from "../server/credential-store";
|
|
22
32
|
|
|
23
|
-
export { ModelResolutionError } from "../model/errors";
|
|
24
33
|
export type { ModelResolutionCode } from "../model/errors";
|
|
34
|
+
export { ModelResolutionError } from "../model/errors";
|
|
25
35
|
export type { ResolvedLanguageModel };
|
|
26
36
|
|
|
27
37
|
/** Options for {@link resolveRegistryLanguageModel}. */
|
|
28
38
|
export interface ResolveRegistryLanguageModelOptions {
|
|
29
|
-
/**
|
|
30
|
-
|
|
39
|
+
/** The one merged registry snapshot the selector resolves against. */
|
|
40
|
+
registry: MergedAIProviderRegistry;
|
|
31
41
|
/**
|
|
32
42
|
* The environment `env:` credential references are read from. Required, so a
|
|
33
43
|
* host never reads the ambient process by accident.
|
|
@@ -40,48 +50,91 @@ export interface ResolveRegistryLanguageModelOptions {
|
|
|
40
50
|
* before anything is read; a refused one is a `policy_refused` failure.
|
|
41
51
|
*/
|
|
42
52
|
policy?: CredentialPolicy;
|
|
43
|
-
/**
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
53
|
+
/** Local encrypted store for `keyring:` references; else `FORGE_CREDENTIAL_STORE` of `env`. */
|
|
54
|
+
path?: string;
|
|
55
|
+
/** 64-char hex key of the local store; else `FORGE_CREDENTIAL_KEY` of `env`. */
|
|
56
|
+
key?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The credential resolver discovery and invocation share: the policy refuses
|
|
61
|
+
* an entry before any reference is read, then the reference resolves from
|
|
62
|
+
* `env` or the local store. Secret-free errors only.
|
|
63
|
+
*/
|
|
64
|
+
function policyCredentialResolver(
|
|
65
|
+
options: Pick<ResolveRegistryLanguageModelOptions, "env" | "policy" | "path" | "key">,
|
|
66
|
+
): EntryCredentialResolver {
|
|
67
|
+
return (entry, providerId) => {
|
|
68
|
+
assertEntryAllowed(entry, providerId, options.policy);
|
|
69
|
+
const resolved = resolveEntryCredential(entry, credentialStoreOptions(options.env, options));
|
|
70
|
+
return resolved.error && !resolved.value
|
|
71
|
+
? { error: resolved.error }
|
|
72
|
+
: { ...(resolved.value !== undefined ? { value: resolved.value } : {}) };
|
|
73
|
+
};
|
|
49
74
|
}
|
|
50
75
|
|
|
51
76
|
/**
|
|
52
77
|
* The AI SDK model for `selector` (`<providerId>/<modelId>`, a bare model id or
|
|
53
|
-
* `@role`), resolved
|
|
54
|
-
* `registry`.
|
|
78
|
+
* `@role`), resolved against `options.registry` only.
|
|
55
79
|
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
80
|
+
* Strict: a selector naming a provider that is missing from the registry's
|
|
81
|
+
* enabled `llm` entries (and that no entry maps as a model id), disabled, or an
|
|
82
|
+
* unassigned `@role` fails — nothing is rerouted to another endpoint and
|
|
83
|
+
* nothing is sent. A bare model id without `/` resolves as the registry orders it.
|
|
84
|
+
*
|
|
85
|
+
* @throws {ModelResolutionError} `unknown_selector` (no enabled entry serves the
|
|
86
|
+
* selector, or the named entry is disabled / not an `llm` entry) ·
|
|
87
|
+
* `policy_refused` · `unresolved` (credential, endpoint or provider SDK; the
|
|
88
|
+
* message is fixed, the underlying error stays on `cause`).
|
|
60
89
|
*/
|
|
61
90
|
export async function resolveRegistryLanguageModel(
|
|
62
91
|
selector: string,
|
|
63
92
|
options: ResolveRegistryLanguageModelOptions,
|
|
64
93
|
): Promise<ResolvedLanguageModel> {
|
|
65
|
-
const merged = options.registry
|
|
94
|
+
const merged = options.registry;
|
|
95
|
+
|
|
96
|
+
const head = selectorProviderId(selector);
|
|
97
|
+
const named = head === undefined ? undefined : merged.providers[head];
|
|
98
|
+
if (
|
|
99
|
+
head !== undefined &&
|
|
100
|
+
named !== undefined &&
|
|
101
|
+
(named.kind !== "llm" || named.enabled === false)
|
|
102
|
+
) {
|
|
103
|
+
throw new ModelResolutionError(
|
|
104
|
+
"unknown_selector",
|
|
105
|
+
`provider '${head}' is ${named.kind !== "llm" ? "not an llm entry" : "disabled"}`,
|
|
106
|
+
{ providerId: head },
|
|
107
|
+
);
|
|
108
|
+
}
|
|
66
109
|
|
|
67
110
|
const ref = parseModelRef(selector, merged, options.roles);
|
|
68
|
-
|
|
111
|
+
// `<head>/<model>` that no entry is named by or maps: the general resolver would
|
|
112
|
+
// hand it to the first entry in registry order, i.e. send it (and that entry's
|
|
113
|
+
// credential) to an endpoint the caller did not name.
|
|
114
|
+
if (!ref || (head !== undefined && named === undefined && ref.fallback === true)) {
|
|
69
115
|
throw new ModelResolutionError("unknown_selector", `no provider serves '${selector}'`);
|
|
70
116
|
}
|
|
117
|
+
if (ref.role !== undefined && !options.roles?.[ref.role]) {
|
|
118
|
+
throw new ModelResolutionError(
|
|
119
|
+
"unknown_selector",
|
|
120
|
+
`role '@${ref.role}' is not assigned to a model`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
71
123
|
const entry = merged.providers[ref.providerId];
|
|
72
124
|
if (entry) assertEntryAllowed(entry, ref.providerId, options.policy);
|
|
73
125
|
|
|
74
126
|
try {
|
|
75
127
|
return await resolveLanguageModel(selector, {
|
|
76
|
-
|
|
128
|
+
registry: merged,
|
|
77
129
|
env: options.env,
|
|
78
130
|
...(options.roles ? { roles: options.roles } : {}),
|
|
79
|
-
|
|
131
|
+
resolveCredential: policyCredentialResolver(options),
|
|
80
132
|
});
|
|
81
133
|
} catch (cause) {
|
|
82
|
-
|
|
83
|
-
//
|
|
84
|
-
//
|
|
134
|
+
if (cause instanceof ModelResolutionError) throw cause;
|
|
135
|
+
// The constructor's message is not ours to vouch for: an SDK error can echo
|
|
136
|
+
// an endpoint with a key in its query. The message is fixed; the original
|
|
137
|
+
// error stays on `cause` for a host that logs it server-side.
|
|
85
138
|
throw new ModelResolutionError(
|
|
86
139
|
"unresolved",
|
|
87
140
|
`provider '${ref.providerId}': credential, endpoint or provider SDK could not be resolved`,
|
|
@@ -89,15 +142,3 @@ export async function resolveRegistryLanguageModel(
|
|
|
89
142
|
);
|
|
90
143
|
}
|
|
91
144
|
}
|
|
92
|
-
|
|
93
|
-
/** The merged registry of `projectDir`; `registry_invalid` on any error diagnostic. */
|
|
94
|
-
function loadRegistry(projectDir: string): MergedAIProviderRegistry {
|
|
95
|
-
const { merged, diagnostics } = loadMergedProviderRegistry({ projectDir });
|
|
96
|
-
const errors = diagnostics
|
|
97
|
-
.filter((d) => d.severity === "error")
|
|
98
|
-
.map((d) => (d.path ? `${d.path}: ${d.message}` : d.message));
|
|
99
|
-
if (errors.length > 0) {
|
|
100
|
-
throw new ModelResolutionError("registry_invalid", `providers registry: ${errors.join("; ")}`);
|
|
101
|
-
}
|
|
102
|
-
return merged;
|
|
103
|
-
}
|
|
@@ -709,7 +709,7 @@
|
|
|
709
709
|
|
|
710
710
|
<script setup lang="ts">
|
|
711
711
|
import { computed } from "vue";
|
|
712
|
-
import type {
|
|
712
|
+
import type { AIProviderDataZoneRule } from "../shared/provider-registry";
|
|
713
713
|
import type { ProvidersPanelClient } from "../vue/providers-client";
|
|
714
714
|
import {
|
|
715
715
|
AZURE_ACCOUNT,
|
|
@@ -754,7 +754,7 @@ const props = withDefaults(
|
|
|
754
754
|
* The host's data-zone rule: rows outside it show a muted note naming the
|
|
755
755
|
* rule (`provider-blocked-<id>`). Absent or `any`: none.
|
|
756
756
|
*/
|
|
757
|
-
dataZoneRule?:
|
|
757
|
+
dataZoneRule?: AIProviderDataZoneRule;
|
|
758
758
|
}>(),
|
|
759
759
|
// Without a default Vue casts an absent boolean to `false`; the panel tells absent from `false`.
|
|
760
760
|
{ environmentEnabled: undefined, note: undefined, expert: false },
|