@kal-elsam/kairo-runtime 0.1.4 → 0.2.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/README.md +23 -0
- package/package.json +1 -1
- package/src/cli.js +113 -4
- package/src/global/action-planner.js +230 -0
- package/src/global/agent-capabilities/create-capability-adapter.js +224 -0
- package/src/global/agent-capabilities/index.js +54 -0
- package/src/global/brand/cli.js +11 -7
- package/src/global/capability-registry.js +80 -0
- package/src/global/capability-states.js +30 -0
- package/src/global/cli-probe.js +47 -0
- package/src/global/ink/orchestrator-app.js +224 -0
- package/src/global/ink/orchestrator-state.js +107 -0
- package/src/global/ink/run-orchestrator-ink.js +29 -0
- package/src/global/intelligence/backends/custom-http.js +175 -0
- package/src/global/intelligence/backends/ollama.js +164 -0
- package/src/global/intelligence/backends/openrouter.js +198 -0
- package/src/global/intelligence/context-compiler.js +338 -0
- package/src/global/intelligence/custom-url.js +82 -0
- package/src/global/intelligence/http.js +63 -0
- package/src/global/intelligence/index.js +38 -0
- package/src/global/intelligence/orchestrate.js +189 -0
- package/src/global/intelligence/registry.js +77 -0
- package/src/global/intelligence/router.js +191 -0
- package/src/global/intelligence/types.js +99 -0
- package/src/global/intelligence-cli.js +323 -0
- package/src/global/orchestrator.js +150 -0
- package/src/global/paths.js +1 -0
- package/src/global/profile.js +297 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
2
|
+
import { resolveHomeDir } from "./paths.js";
|
|
3
|
+
import { runGlobalSetup } from "./global-cli.js";
|
|
4
|
+
import { PLAN_ACTIONS, buildReadOnlyDiagnostics, shouldExecutePlan } from "./action-planner.js";
|
|
5
|
+
import { canUseOrchestratorShell } from "./ink/orchestrator-state.js";
|
|
6
|
+
import { runOrchestratorInk as defaultRunOrchestratorInk } from "./ink/run-orchestrator-ink.js";
|
|
7
|
+
import { formatCliCommand } from "./brand/cli.js";
|
|
8
|
+
import { BRAND } from "./brand/index.js";
|
|
9
|
+
|
|
10
|
+
export { canUseOrchestratorShell };
|
|
11
|
+
|
|
12
|
+
export function shouldOpenOrchestratorShell({
|
|
13
|
+
interactive = Boolean(input.isTTY && output.isTTY),
|
|
14
|
+
json = false,
|
|
15
|
+
hasImplicitFlags = false
|
|
16
|
+
} = {}) {
|
|
17
|
+
if (!interactive || json) return false;
|
|
18
|
+
if (hasImplicitFlags) return false;
|
|
19
|
+
return canUseOrchestratorShell({ interactive });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function runOrchestratorShell({
|
|
23
|
+
packageRoot,
|
|
24
|
+
packageManifest,
|
|
25
|
+
workspaceRoot,
|
|
26
|
+
interactive = Boolean(input.isTTY && output.isTTY),
|
|
27
|
+
runOrchestratorInkImpl = defaultRunOrchestratorInk
|
|
28
|
+
}) {
|
|
29
|
+
if (!interactive) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
`Non-interactive shell requires an explicit command. Try ${formatCliCommand("help")} or ${formatCliCommand("setup --yes")}.`
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!canUseOrchestratorShell({ interactive })) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`Interactive shell requires a capable TTY. Use ${formatCliCommand("setup --simple")} or explicit commands.`
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const homeDir = resolveHomeDir();
|
|
42
|
+
const outcome = await runOrchestratorInkImpl({
|
|
43
|
+
homeDir,
|
|
44
|
+
workspaceRoot,
|
|
45
|
+
packageRoot,
|
|
46
|
+
packageName: packageManifest.name,
|
|
47
|
+
cliVersion: packageManifest.version
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
if (outcome.error) {
|
|
51
|
+
throw outcome.error;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (outcome.cancelled) {
|
|
55
|
+
return { cancelled: true, wrote: false };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (outcome.confirmed && outcome.action === PLAN_ACTIONS.SETUP) {
|
|
59
|
+
const setupOutcome = await runGlobalSetup(
|
|
60
|
+
{
|
|
61
|
+
cwd: workspaceRoot,
|
|
62
|
+
adapters: null,
|
|
63
|
+
components: null,
|
|
64
|
+
noDefaultComponents: false,
|
|
65
|
+
dryRun: false,
|
|
66
|
+
yes: false,
|
|
67
|
+
confirm: false,
|
|
68
|
+
preflight: true,
|
|
69
|
+
preflightExplicit: false,
|
|
70
|
+
yesExplicit: false,
|
|
71
|
+
confirmExplicit: false,
|
|
72
|
+
json: false,
|
|
73
|
+
interactive: true,
|
|
74
|
+
simple: false
|
|
75
|
+
},
|
|
76
|
+
packageManifest,
|
|
77
|
+
packageRoot
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
cancelled: Boolean(setupOutcome.cancelled),
|
|
82
|
+
wrote: !setupOutcome.cancelled && !setupOutcome.result?.dryRun,
|
|
83
|
+
action: PLAN_ACTIONS.SETUP,
|
|
84
|
+
setupOutcome
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return { cancelled: false, wrote: false, action: outcome.action ?? null };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function runOrchestratorDiagnostics({
|
|
92
|
+
homeDir,
|
|
93
|
+
workspaceRoot,
|
|
94
|
+
packageName,
|
|
95
|
+
packageRoot,
|
|
96
|
+
cliVersion,
|
|
97
|
+
json = false
|
|
98
|
+
}) {
|
|
99
|
+
const diagnostics = await buildReadOnlyDiagnostics({
|
|
100
|
+
homeDir,
|
|
101
|
+
workspaceRoot,
|
|
102
|
+
packageName,
|
|
103
|
+
packageRoot,
|
|
104
|
+
cliVersion
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
if (json) {
|
|
108
|
+
return diagnostics;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
console.log(commandHeader("orchestrator — agent capability diagnostics"));
|
|
112
|
+
console.log(`Home: ${homeDir}`);
|
|
113
|
+
console.log(`Workspace: ${workspaceRoot}`);
|
|
114
|
+
console.log("");
|
|
115
|
+
|
|
116
|
+
for (const capability of diagnostics.capabilities) {
|
|
117
|
+
console.log(
|
|
118
|
+
` ${capability.label.padEnd(14)} ${capability.state.padEnd(14)} detected=${capability.detected ? "yes" : "no"}`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (diagnostics.intelligence) {
|
|
123
|
+
console.log("");
|
|
124
|
+
console.log("Intelligence backends:");
|
|
125
|
+
for (const backend of diagnostics.intelligence.backends) {
|
|
126
|
+
console.log(
|
|
127
|
+
` ${backend.label.padEnd(14)} ${backend.state.padEnd(14)} models=${backend.models?.length ?? 0}`
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
console.log(` Routing: ${diagnostics.intelligence.routingPreview?.reason ?? "n/a"}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
console.log("");
|
|
134
|
+
console.log("Recommendations:");
|
|
135
|
+
for (const recommendation of diagnostics.recommendations) {
|
|
136
|
+
console.log(` - ${recommendation}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return diagnostics;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function commandHeader(title) {
|
|
143
|
+
return `${BRAND.displayName} ${title}`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function assertPlanExecution(plan, { confirmed = false } = {}) {
|
|
147
|
+
if (!shouldExecutePlan(plan, { confirmed })) {
|
|
148
|
+
throw new Error("Plan declined. No writes or installations were performed.");
|
|
149
|
+
}
|
|
150
|
+
}
|
package/src/global/paths.js
CHANGED
|
@@ -15,6 +15,7 @@ export function harnessHomePaths(homeDir) {
|
|
|
15
15
|
root,
|
|
16
16
|
statePath: join(root, "state.json"),
|
|
17
17
|
policyPath: join(root, "policy.json"),
|
|
18
|
+
profilePath: join(root, "profile.json"),
|
|
18
19
|
historyPath: join(root, "history.jsonl"),
|
|
19
20
|
coreDir: join(root, "core"),
|
|
20
21
|
backupsDir: join(root, "backups")
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { harnessHomePaths } from "./paths.js";
|
|
5
|
+
import { AGENT_CAPABILITY_IDS } from "./agent-capabilities/index.js";
|
|
6
|
+
import { classifyCustomBaseUrl, isValidEnvironmentName } from "./intelligence/custom-url.js";
|
|
7
|
+
|
|
8
|
+
export const PROFILE_KEYS = new Set([
|
|
9
|
+
"coordinator",
|
|
10
|
+
"defaultAgents",
|
|
11
|
+
"defaultComponents",
|
|
12
|
+
"applyMode",
|
|
13
|
+
"preferredBackend",
|
|
14
|
+
"preferredModel",
|
|
15
|
+
"cloudConsent",
|
|
16
|
+
"tokenBudget",
|
|
17
|
+
"stableContextBudget",
|
|
18
|
+
"requestContextBudget",
|
|
19
|
+
"customProviders"
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
export const DEFAULT_PROFILE = {
|
|
23
|
+
coordinator: null,
|
|
24
|
+
defaultAgents: "detected",
|
|
25
|
+
defaultComponents: null,
|
|
26
|
+
applyMode: "prompt",
|
|
27
|
+
preferredBackend: null,
|
|
28
|
+
preferredModel: null,
|
|
29
|
+
cloudConsent: false,
|
|
30
|
+
tokenBudget: null,
|
|
31
|
+
stableContextBudget: null,
|
|
32
|
+
requestContextBudget: null,
|
|
33
|
+
customProviders: []
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const APPLY_MODES = new Set(["prompt", "confirm"]);
|
|
37
|
+
|
|
38
|
+
export function getGlobalProfilePath(homeDir) {
|
|
39
|
+
return join(harnessHomePaths(homeDir).root, "profile.json");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function getProjectProfilePath(workspaceRoot) {
|
|
43
|
+
return join(workspaceRoot, ".harness", "kairo.json");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function loadProfileFile(filePath) {
|
|
47
|
+
if (!existsSync(filePath)) return null;
|
|
48
|
+
|
|
49
|
+
let parsed;
|
|
50
|
+
try {
|
|
51
|
+
parsed = JSON.parse(await readFile(filePath, "utf8"));
|
|
52
|
+
} catch (error) {
|
|
53
|
+
throw new Error(`Invalid profile file at ${filePath}: ${error.message}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
57
|
+
throw new Error(`Invalid profile file at ${filePath}: expected a JSON object.`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return parsed;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function loadGlobalProfile(homeDir) {
|
|
64
|
+
return loadProfileFile(getGlobalProfilePath(homeDir));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function loadProjectProfile(workspaceRoot) {
|
|
68
|
+
return loadProfileFile(getProjectProfilePath(workspaceRoot));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function resolveProfile({ homeDir, workspaceRoot }) {
|
|
72
|
+
const globalRaw = await loadGlobalProfile(homeDir);
|
|
73
|
+
const projectRaw = await loadProjectProfile(workspaceRoot);
|
|
74
|
+
|
|
75
|
+
const merged = {
|
|
76
|
+
...DEFAULT_PROFILE,
|
|
77
|
+
...(globalRaw ?? {}),
|
|
78
|
+
...(projectRaw ?? {})
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
validateProfile(merged);
|
|
82
|
+
merged.customProviders = sanitizeCustomProviders(merged.customProviders);
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
profile: merged,
|
|
86
|
+
sources: {
|
|
87
|
+
global: globalRaw ? getGlobalProfilePath(homeDir) : null,
|
|
88
|
+
project: projectRaw ? getProjectProfilePath(workspaceRoot) : null
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function buildProfileJson(resolved) {
|
|
94
|
+
const { profile, sources } = resolved;
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
coordinator: profile.coordinator,
|
|
98
|
+
defaultAgents: profile.defaultAgents,
|
|
99
|
+
defaultComponents: profile.defaultComponents,
|
|
100
|
+
applyMode: profile.applyMode,
|
|
101
|
+
preferredBackend: profile.preferredBackend,
|
|
102
|
+
preferredModel: profile.preferredModel,
|
|
103
|
+
cloudConsent: profile.cloudConsent,
|
|
104
|
+
tokenBudget: profile.tokenBudget,
|
|
105
|
+
stableContextBudget: profile.stableContextBudget,
|
|
106
|
+
requestContextBudget: profile.requestContextBudget,
|
|
107
|
+
customProviders: sanitizeCustomProviders(profile.customProviders),
|
|
108
|
+
sources: {
|
|
109
|
+
global: sources.global,
|
|
110
|
+
project: sources.project,
|
|
111
|
+
precedence: "project overrides global overrides defaults"
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function saveGlobalProfile(homeDir, profile) {
|
|
117
|
+
validateProfile(profile);
|
|
118
|
+
const { root } = harnessHomePaths(homeDir);
|
|
119
|
+
const profilePath = getGlobalProfilePath(homeDir);
|
|
120
|
+
await mkdir(root, { recursive: true });
|
|
121
|
+
await writeFile(profilePath, `${JSON.stringify(profile, null, 2)}\n`, "utf8");
|
|
122
|
+
return profilePath;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function resolveProfileAgents(profile, detectedAgentIds) {
|
|
126
|
+
if (profile.defaultAgents === "detected") {
|
|
127
|
+
return detectedAgentIds.length > 0 ? [...detectedAgentIds] : [...AGENT_CAPABILITY_IDS];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (profile.defaultAgents === "all") {
|
|
131
|
+
return [...AGENT_CAPABILITY_IDS];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (Array.isArray(profile.defaultAgents)) {
|
|
135
|
+
return profile.defaultAgents.filter((id) => AGENT_CAPABILITY_IDS.includes(id));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return [...AGENT_CAPABILITY_IDS];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function validateProfile(profile) {
|
|
142
|
+
if (profile.coordinator != null && !AGENT_CAPABILITY_IDS.includes(profile.coordinator)) {
|
|
143
|
+
throw new Error(`Unknown coordinator "${profile.coordinator}". Use ${AGENT_CAPABILITY_IDS.join(", ")} or null.`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (!APPLY_MODES.has(profile.applyMode)) {
|
|
147
|
+
throw new Error(`Invalid applyMode "${profile.applyMode}". Use prompt or confirm.`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (
|
|
151
|
+
profile.defaultAgents !== "detected"
|
|
152
|
+
&& profile.defaultAgents !== "all"
|
|
153
|
+
&& !Array.isArray(profile.defaultAgents)
|
|
154
|
+
) {
|
|
155
|
+
throw new Error('Profile defaultAgents must be "detected", "all", or an agent list.');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (Array.isArray(profile.defaultAgents)) {
|
|
159
|
+
for (const agent of profile.defaultAgents) {
|
|
160
|
+
if (!AGENT_CAPABILITY_IDS.includes(agent)) {
|
|
161
|
+
throw new Error(`Unknown agent "${agent}" in profile.`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (profile.defaultComponents != null && !Array.isArray(profile.defaultComponents)) {
|
|
167
|
+
throw new Error("Profile defaultComponents must be an array or null.");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (profile.cloudConsent != null && typeof profile.cloudConsent !== "boolean") {
|
|
171
|
+
throw new Error("Profile cloudConsent must be a boolean.");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
for (const key of ["tokenBudget", "stableContextBudget", "requestContextBudget"]) {
|
|
175
|
+
if (profile[key] != null && (!Number.isFinite(profile[key]) || profile[key] < 1)) {
|
|
176
|
+
throw new Error(`Profile ${key} must be a positive number or null.`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (profile.preferredBackend != null && typeof profile.preferredBackend !== "string") {
|
|
181
|
+
throw new Error("Profile preferredBackend must be a string or null.");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (profile.preferredModel != null && typeof profile.preferredModel !== "string") {
|
|
185
|
+
throw new Error("Profile preferredModel must be a string or null.");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
validateNoSecrets(profile);
|
|
189
|
+
validateCustomProviders(profile.customProviders);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Secret-looking key segments after camelCase → snake_case normalization.
|
|
194
|
+
* `api_key_env` is the only credential-related key allowed (env var name, not a secret).
|
|
195
|
+
*/
|
|
196
|
+
const SECRET_KEY_PATTERN = /(^|_)(api_?key|api_?token|access_?token|auth_?token|client_?secret|private_?key|authorization(_header)?|password|secrets?|credentials?|bearer|token)(_|$)/;
|
|
197
|
+
const SECRET_KEY_ALLOWLIST = new Set([
|
|
198
|
+
"api_key_env",
|
|
199
|
+
"token_budget",
|
|
200
|
+
"stable_context_budget",
|
|
201
|
+
"request_context_budget"
|
|
202
|
+
]);
|
|
203
|
+
const SECRET_VALUE_PATTERN = /^(sk-[A-Za-z0-9]|sk-or-|gh[pousr]_|xox[baprs]-|AKIA[0-9A-Z]{16}\b|Bearer\s+\S+|eyJ[A-Za-z0-9_-]+\.)|-----BEGIN [A-Z ]*PRIVATE KEY-----/i;
|
|
204
|
+
|
|
205
|
+
export function normalizeProfileKey(key) {
|
|
206
|
+
return String(key)
|
|
207
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
|
|
208
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2")
|
|
209
|
+
.replace(/-/g, "_")
|
|
210
|
+
.toLowerCase();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function isForbiddenSecretKey(key) {
|
|
214
|
+
const normalized = normalizeProfileKey(key);
|
|
215
|
+
if (SECRET_KEY_ALLOWLIST.has(normalized)) return false;
|
|
216
|
+
return SECRET_KEY_PATTERN.test(normalized);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function validateNoSecrets(profile) {
|
|
220
|
+
walkForSecrets(profile);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function walkForSecrets(value) {
|
|
224
|
+
if (typeof value === "string") {
|
|
225
|
+
if (SECRET_VALUE_PATTERN.test(value)) {
|
|
226
|
+
throw new Error("Profile must not store credential-like values. Use environment variables.");
|
|
227
|
+
}
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (value == null || typeof value !== "object") return;
|
|
231
|
+
|
|
232
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
233
|
+
const normalizedKey = normalizeProfileKey(key);
|
|
234
|
+
|
|
235
|
+
// apiKeyEnv is the only allowed credential-related key: it names an env var.
|
|
236
|
+
if (normalizedKey === "api_key_env") {
|
|
237
|
+
if (typeof nested === "string" && SECRET_VALUE_PATTERN.test(nested)) {
|
|
238
|
+
throw new Error("Profile must not store credential-like values. Use environment variables.");
|
|
239
|
+
}
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (isForbiddenSecretKey(key)) {
|
|
244
|
+
throw new Error(`Profile must not store credentials (rejected key "${key}"). Use environment variables.`);
|
|
245
|
+
}
|
|
246
|
+
if (typeof nested === "string" && SECRET_VALUE_PATTERN.test(nested)) {
|
|
247
|
+
throw new Error("Profile must not store credential-like values. Use environment variables.");
|
|
248
|
+
}
|
|
249
|
+
walkForSecrets(nested);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function validateCustomProviders(providers) {
|
|
254
|
+
if (providers == null) return;
|
|
255
|
+
if (!Array.isArray(providers)) {
|
|
256
|
+
throw new Error("Profile customProviders must be an array.");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
for (const provider of providers) {
|
|
260
|
+
if (provider == null || typeof provider !== "object" || Array.isArray(provider)) {
|
|
261
|
+
throw new Error("Each customProviders entry must be an object.");
|
|
262
|
+
}
|
|
263
|
+
if (!provider.baseUrl || typeof provider.baseUrl !== "string") {
|
|
264
|
+
throw new Error("customProviders entries require baseUrl.");
|
|
265
|
+
}
|
|
266
|
+
for (const key of Object.keys(provider)) {
|
|
267
|
+
if (normalizeProfileKey(key) === "api_key_env") continue;
|
|
268
|
+
if (isForbiddenSecretKey(key)) {
|
|
269
|
+
throw new Error(`customProviders must not include "${key}". Use apiKeyEnv to name an environment variable.`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if (provider.apiKey != null || provider.token != null || provider.secret != null) {
|
|
273
|
+
throw new Error("customProviders must not embed secrets. Set apiKeyEnv to an environment variable name.");
|
|
274
|
+
}
|
|
275
|
+
if (provider.apiKeyEnv != null && !isValidEnvironmentName(provider.apiKeyEnv)) {
|
|
276
|
+
throw new Error("customProviders apiKeyEnv must be a valid uppercase environment variable name.");
|
|
277
|
+
}
|
|
278
|
+
const location = classifyCustomBaseUrl(provider.baseUrl);
|
|
279
|
+
if (provider.apiKeyEnv && !location.local) {
|
|
280
|
+
throw new Error(
|
|
281
|
+
"Remote custom providers cannot use apiKeyEnv in 0.2.0; use a built-in provider or a local endpoint."
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function sanitizeCustomProviders(providers) {
|
|
288
|
+
if (!Array.isArray(providers)) return [];
|
|
289
|
+
return providers.map((provider) => ({
|
|
290
|
+
id: provider.id ?? "custom",
|
|
291
|
+
label: provider.label ?? "Custom provider",
|
|
292
|
+
baseUrl: provider.baseUrl,
|
|
293
|
+
modelId: provider.modelId ?? null,
|
|
294
|
+
apiKeyEnv: provider.apiKeyEnv ?? null,
|
|
295
|
+
local: classifyCustomBaseUrl(provider.baseUrl).local
|
|
296
|
+
}));
|
|
297
|
+
}
|