@stigmer/cli 3.3.0 → 3.4.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/commands/delete.js +5 -2
- package/commands/delete.js.map +1 -1
- package/commands/setup.d.ts.map +1 -1
- package/commands/setup.js +11 -16
- package/commands/setup.js.map +1 -1
- package/local/daemon/components.d.ts.map +1 -1
- package/local/daemon/components.js +3 -2
- package/local/daemon/components.js.map +1 -1
- package/local/daemon/env.d.ts +0 -3
- package/local/daemon/env.d.ts.map +1 -1
- package/local/daemon/env.js +4 -8
- package/local/daemon/env.js.map +1 -1
- package/local/daemon/launch.d.ts.map +1 -1
- package/local/daemon/launch.js +9 -17
- package/local/daemon/launch.js.map +1 -1
- package/local/llm-config.d.ts +7 -7
- package/local/llm-config.d.ts.map +1 -1
- package/local/llm-config.js +29 -42
- package/local/llm-config.js.map +1 -1
- package/local/setup/wizard.d.ts +16 -7
- package/local/setup/wizard.d.ts.map +1 -1
- package/local/setup/wizard.js +41 -44
- package/local/setup/wizard.js.map +1 -1
- package/local/status.d.ts.map +1 -1
- package/local/status.js +16 -15
- package/local/status.js.map +1 -1
- package/package.json +5 -5
- package/resources/delete.d.ts +5 -1
- package/resources/delete.d.ts.map +1 -1
- package/resources/delete.js +40 -8
- package/resources/delete.js.map +1 -1
- package/resources/get-bindings.d.ts.map +1 -1
- package/resources/get-bindings.js +8 -0
- package/resources/get-bindings.js.map +1 -1
- package/resources/list.d.ts.map +1 -1
- package/resources/list.js +70 -4
- package/resources/list.js.map +1 -1
- package/src/commands/delete.ts +5 -2
- package/src/commands/setup.ts +13 -20
- package/src/local/daemon/components.test.ts +8 -2
- package/src/local/daemon/components.ts +3 -1
- package/src/local/daemon/env.test.ts +13 -6
- package/src/local/daemon/env.ts +10 -13
- package/src/local/daemon/launch.ts +9 -17
- package/src/local/llm-config.test.ts +16 -27
- package/src/local/llm-config.ts +27 -41
- package/src/local/setup/wizard.test.ts +19 -11
- package/src/local/setup/wizard.ts +42 -47
- package/src/local/status.test.ts +20 -0
- package/src/local/status.ts +17 -15
- package/src/resources/delete.ts +48 -9
- package/src/resources/get-bindings.ts +8 -0
- package/src/resources/list.ts +76 -4
- package/src/resources/mutate-verbs.integration.test.ts +178 -1
- package/src/resources/read-verbs.integration.test.ts +236 -2
package/src/commands/setup.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
// `stigmer setup` — configure the LLM provider used for agent execution.
|
|
2
2
|
//
|
|
3
3
|
// Two paths share one decision core (local/setup/wizard.ts): a non-interactive
|
|
4
|
-
// flag path (`--provider`, `--api-key`,
|
|
5
|
-
// interactive
|
|
6
|
-
// backend.local.llm, preserving every other config field.
|
|
4
|
+
// flag path (`--provider`, `--api-key`, `--model`) for scripts and CI, and an
|
|
5
|
+
// interactive API-key prompt when no provider flag is given. Both write to
|
|
6
|
+
// backend.local.llm, preserving every other config field. The valid provider
|
|
7
|
+
// set is owned by wizard.ts's PROVIDER_CHOICES (Anthropic-only + skip — the
|
|
8
|
+
// only story the native runner serves).
|
|
7
9
|
|
|
8
10
|
import type { Command } from "commander";
|
|
9
11
|
import { homedir } from "node:os";
|
|
@@ -14,51 +16,42 @@ interface SetupFlags extends OutputFlags {
|
|
|
14
16
|
provider?: string;
|
|
15
17
|
model?: string;
|
|
16
18
|
apiKey?: string;
|
|
17
|
-
baseUrl?: string;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
const PROVIDERS = ["anthropic", "openai", "ollama", "skip"] as const;
|
|
21
|
-
|
|
22
21
|
export function registerSetup(program: Command): void {
|
|
23
22
|
const setup = program
|
|
24
23
|
.command("setup")
|
|
25
24
|
.description("configure the LLM provider for agent execution")
|
|
26
|
-
.option("--provider <name>", "provider: anthropic,
|
|
27
|
-
.option("--model <name>", "model
|
|
28
|
-
.option("--api-key <key>", "API key
|
|
29
|
-
.option("--base-url <url>", "custom base URL (e.g. an Ollama endpoint)")
|
|
25
|
+
.option("--provider <name>", "provider: anthropic, or skip to clear (non-interactive)")
|
|
26
|
+
.option("--model <name>", "pin a specific model (defaults to the platform-selected model)")
|
|
27
|
+
.option("--api-key <key>", "Anthropic API key")
|
|
30
28
|
.action((options: SetupFlags) => runSetup(options));
|
|
31
29
|
addResultFlags(setup);
|
|
32
30
|
}
|
|
33
31
|
|
|
34
32
|
async function runSetup(options: SetupFlags): Promise<void> {
|
|
35
33
|
const { load, save } = await import("../config/config.js");
|
|
36
|
-
const { applyChoice, runInteractiveWizard } = await import("../local/setup/wizard.js");
|
|
34
|
+
const { PROVIDER_CHOICES, applyChoice, runInteractiveWizard } = await import("../local/setup/wizard.js");
|
|
37
35
|
const { isRunning } = await import("../local/daemon/launch.js");
|
|
38
36
|
const { CliExitError } = await import("../errors/cli-exit-error.js");
|
|
39
37
|
const { ExitCode } = await import("../errors/exit-codes.js");
|
|
40
38
|
|
|
41
39
|
const config = load();
|
|
42
40
|
|
|
43
|
-
let updated: typeof config
|
|
41
|
+
let updated: typeof config;
|
|
44
42
|
if (options.provider !== undefined) {
|
|
45
43
|
const provider = options.provider.toLowerCase();
|
|
46
|
-
if (!
|
|
44
|
+
if (!(PROVIDER_CHOICES as readonly string[]).includes(provider)) {
|
|
47
45
|
throw new CliExitError(`unknown provider: ${options.provider}`, ExitCode.Usage, [
|
|
48
|
-
`Valid providers: ${
|
|
46
|
+
`Valid providers: ${PROVIDER_CHOICES.join(", ")}`,
|
|
49
47
|
]);
|
|
50
48
|
}
|
|
51
|
-
updated = applyChoice(config, provider as (typeof
|
|
49
|
+
updated = applyChoice(config, provider as (typeof PROVIDER_CHOICES)[number], {
|
|
52
50
|
model: options.model,
|
|
53
51
|
apiKey: options.apiKey,
|
|
54
|
-
baseUrl: options.baseUrl,
|
|
55
52
|
});
|
|
56
53
|
} else {
|
|
57
54
|
updated = await runInteractiveWizard(config);
|
|
58
|
-
if (updated === null) {
|
|
59
|
-
renderResult(CommandResult.warning("Setup cancelled"), resultFormat(options));
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
55
|
}
|
|
63
56
|
|
|
64
57
|
save(updated);
|
|
@@ -47,10 +47,9 @@ describe("buildRunnerEnv", () => {
|
|
|
47
47
|
expect(withKey.STIGMER_ACTIVITY_ROUTING).toBe("session");
|
|
48
48
|
});
|
|
49
49
|
|
|
50
|
-
it("forwards
|
|
50
|
+
it("forwards the Anthropic key only when set, resolved value winning over the base env", () => {
|
|
51
51
|
const bare = buildRunnerEnv(config, {});
|
|
52
52
|
expect(bare.ANTHROPIC_API_KEY).toBeUndefined();
|
|
53
|
-
expect(bare.OPENAI_API_KEY).toBeUndefined();
|
|
54
53
|
|
|
55
54
|
// The contract value is authoritative: the launcher already applied the
|
|
56
55
|
// env > config precedence, so whatever sits in the child's base env must
|
|
@@ -62,6 +61,13 @@ describe("buildRunnerEnv", () => {
|
|
|
62
61
|
expect(env.ANTHROPIC_API_KEY).toBe("sk-ant-resolved");
|
|
63
62
|
});
|
|
64
63
|
|
|
64
|
+
// Anthropic is the only key with a contract slot; other provider keys (for
|
|
65
|
+
// advanced per-agent overrides) flow through shell-env inheritance untouched.
|
|
66
|
+
it("passes other shell-exported provider keys through via the base env spread", () => {
|
|
67
|
+
const env = buildRunnerEnv(config, { OPENAI_API_KEY: "sk-oai-shell" });
|
|
68
|
+
expect(env.OPENAI_API_KEY).toBe("sk-oai-shell");
|
|
69
|
+
});
|
|
70
|
+
|
|
65
71
|
// The regression guard for the queue bug: the server dispatches to, and the
|
|
66
72
|
// runner polls, the exact same queue.
|
|
67
73
|
it("pins the runner's poll queue to the server's dispatch queue", () => {
|
|
@@ -53,8 +53,10 @@ export function buildRunnerEnv(config: DaemonConfig, base: NodeJS.ProcessEnv = p
|
|
|
53
53
|
if (config.cursorApiKey !== undefined) env.CURSOR_API_KEY = config.cursorApiKey;
|
|
54
54
|
// Explicit set (after the base spread) so the launcher-resolved key wins over any
|
|
55
55
|
// stale inherited value — the delivery path for a `stigmer setup`-persisted key.
|
|
56
|
+
// Anthropic only: it is the sole provider local execution supports. Other keys
|
|
57
|
+
// (e.g. a shell-exported OPENAI_API_KEY for advanced per-agent gpt-* overrides)
|
|
58
|
+
// still reach the runner through the base env spread above.
|
|
56
59
|
if (config.anthropicApiKey !== undefined) env.ANTHROPIC_API_KEY = config.anthropicApiKey;
|
|
57
|
-
if (config.openaiApiKey !== undefined) env.OPENAI_API_KEY = config.openaiApiKey;
|
|
58
60
|
if (config.activityRouting !== undefined) env.STIGMER_ACTIVITY_ROUTING = config.activityRouting;
|
|
59
61
|
return env;
|
|
60
62
|
}
|
|
@@ -27,7 +27,6 @@ describe("buildDaemonEnv + readDaemonConfig", () => {
|
|
|
27
27
|
runner: { nodeBin: "/usr/bin/node", entryPath: "/repo/runner/dist/main.js", appDir: "/repo/runner" },
|
|
28
28
|
cursorApiKey: undefined,
|
|
29
29
|
anthropicApiKey: undefined,
|
|
30
|
-
openaiApiKey: undefined,
|
|
31
30
|
activityRouting: undefined,
|
|
32
31
|
});
|
|
33
32
|
});
|
|
@@ -48,18 +47,26 @@ describe("buildDaemonEnv + readDaemonConfig", () => {
|
|
|
48
47
|
|
|
49
48
|
// The config-file-only delivery path: a key from `stigmer setup` is not in the
|
|
50
49
|
// shell env, so the launcher must write it into the contract explicitly.
|
|
51
|
-
|
|
50
|
+
// Anthropic is the only provider with this path — the local stack executes
|
|
51
|
+
// on Anthropic only.
|
|
52
|
+
it("delivers a launcher-resolved Anthropic key with no help from the base env", () => {
|
|
52
53
|
const env = buildDaemonEnv({ ...baseInputs, anthropicApiKey: "sk-ant-cfg" }, {});
|
|
53
54
|
const config = readDaemonConfig(env);
|
|
54
55
|
expect(config.anthropicApiKey).toBe("sk-ant-cfg");
|
|
55
|
-
expect(config.openaiApiKey).toBeUndefined();
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
-
it("passes shell-exported
|
|
59
|
-
const env = buildDaemonEnv(baseInputs, { ANTHROPIC_API_KEY: "sk-ant-env"
|
|
58
|
+
it("passes a shell-exported Anthropic key through from the base env", () => {
|
|
59
|
+
const env = buildDaemonEnv(baseInputs, { ANTHROPIC_API_KEY: "sk-ant-env" });
|
|
60
60
|
const config = readDaemonConfig(env);
|
|
61
61
|
expect(config.anthropicApiKey).toBe("sk-ant-env");
|
|
62
|
-
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Other provider keys have no contract slot; they reach the runner solely via
|
|
65
|
+
// shell-env inheritance (the child env spreads the base env).
|
|
66
|
+
it("leaves shell-exported non-Anthropic keys in the env without parsing them", () => {
|
|
67
|
+
const env = buildDaemonEnv(baseInputs, { OPENAI_API_KEY: "sk-oai-env" });
|
|
68
|
+
expect(env.OPENAI_API_KEY).toBe("sk-oai-env");
|
|
69
|
+
expect(readDaemonConfig(env)).not.toHaveProperty("openaiApiKey");
|
|
63
70
|
});
|
|
64
71
|
|
|
65
72
|
it("requires the data dir and server binary", () => {
|
package/src/local/daemon/env.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// The foreground launcher hands the supervised daemon everything it needs to
|
|
2
|
-
// run through the environment
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
2
|
+
// run through the environment. Centralizing the variable names and the
|
|
3
|
+
// (de)serialization here keeps that contract in one place: the launcher writes
|
|
4
|
+
// it with buildDaemonEnv, the daemon reads it with readDaemonConfig, and
|
|
5
|
+
// nothing in between guesses at string keys.
|
|
6
6
|
|
|
7
7
|
/** Environment variable names that make up the launcher -> daemon contract. */
|
|
8
8
|
export const DaemonEnvVar = {
|
|
@@ -18,7 +18,6 @@ export const DaemonEnvVar = {
|
|
|
18
18
|
RunnerAppDir: "STIGMER_RUNNER_APP_DIR",
|
|
19
19
|
CursorApiKey: "CURSOR_API_KEY",
|
|
20
20
|
AnthropicApiKey: "ANTHROPIC_API_KEY",
|
|
21
|
-
OpenAiApiKey: "OPENAI_API_KEY",
|
|
22
21
|
ActivityRouting: "STIGMER_ACTIVITY_ROUTING",
|
|
23
22
|
} as const;
|
|
24
23
|
|
|
@@ -41,7 +40,6 @@ export interface DaemonConfig {
|
|
|
41
40
|
runner?: RunnerLaunch;
|
|
42
41
|
cursorApiKey?: string;
|
|
43
42
|
anthropicApiKey?: string;
|
|
44
|
-
openaiApiKey?: string;
|
|
45
43
|
activityRouting?: string;
|
|
46
44
|
}
|
|
47
45
|
|
|
@@ -55,12 +53,13 @@ export interface DaemonEnvInputs {
|
|
|
55
53
|
noWeb: boolean;
|
|
56
54
|
serverBin: string;
|
|
57
55
|
runner?: RunnerLaunch;
|
|
58
|
-
//
|
|
59
|
-
// into the daemon env explicitly: unlike a shell-exported key, a key
|
|
60
|
-
// `stigmer setup` exists only in the config file and would
|
|
61
|
-
// the runner.
|
|
56
|
+
// Anthropic API key resolved by the launcher (env > config file). Must be
|
|
57
|
+
// written into the daemon env explicitly: unlike a shell-exported key, a key
|
|
58
|
+
// persisted by `stigmer setup` exists only in the config file and would
|
|
59
|
+
// otherwise never reach the runner. Anthropic is the only provider with a
|
|
60
|
+
// persisted delivery path — other keys (OPENAI_API_KEY, CURSOR_API_KEY)
|
|
61
|
+
// reach the runner solely via shell-env inheritance.
|
|
62
62
|
anthropicApiKey?: string;
|
|
63
|
-
openaiApiKey?: string;
|
|
64
63
|
}
|
|
65
64
|
|
|
66
65
|
/**
|
|
@@ -83,7 +82,6 @@ export function buildDaemonEnv(inputs: DaemonEnvInputs, base: NodeJS.ProcessEnv
|
|
|
83
82
|
env[DaemonEnvVar.RunnerAppDir] = inputs.runner.appDir;
|
|
84
83
|
}
|
|
85
84
|
if (inputs.anthropicApiKey !== undefined) env[DaemonEnvVar.AnthropicApiKey] = inputs.anthropicApiKey;
|
|
86
|
-
if (inputs.openaiApiKey !== undefined) env[DaemonEnvVar.OpenAiApiKey] = inputs.openaiApiKey;
|
|
87
85
|
return env;
|
|
88
86
|
}
|
|
89
87
|
|
|
@@ -106,7 +104,6 @@ export function readDaemonConfig(env: NodeJS.ProcessEnv = process.env): DaemonCo
|
|
|
106
104
|
runner: serverOnly ? undefined : runner,
|
|
107
105
|
cursorApiKey: nonEmpty(env[DaemonEnvVar.CursorApiKey]),
|
|
108
106
|
anthropicApiKey: nonEmpty(env[DaemonEnvVar.AnthropicApiKey]),
|
|
109
|
-
openaiApiKey: nonEmpty(env[DaemonEnvVar.OpenAiApiKey]),
|
|
110
107
|
activityRouting: nonEmpty(env[DaemonEnvVar.ActivityRouting]),
|
|
111
108
|
};
|
|
112
109
|
}
|
|
@@ -99,9 +99,8 @@ export async function up(options: UpOptions = {}, home: string = homedir()): Pro
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
/**
|
|
102
|
-
* Bootstrap the system seedpack into the freshly-started local backend
|
|
103
|
-
*
|
|
104
|
-
* content-hash marker, so repeated `up`s are cheap. Best-effort: the stack is
|
|
102
|
+
* Bootstrap the system seedpack into the freshly-started local backend.
|
|
103
|
+
* Idempotent via a content-hash marker, so repeated `up`s are cheap. Best-effort: the stack is
|
|
105
104
|
* already serving by this point, so a seedpack failure is surfaced as a warning
|
|
106
105
|
* with a retry hint rather than tearing the stack down.
|
|
107
106
|
*/
|
|
@@ -228,23 +227,16 @@ async function stopManagedTemporal(home: string): Promise<void> {
|
|
|
228
227
|
// the config file, so the launcher must write it into the daemon contract explicitly
|
|
229
228
|
// — unlike a shell-exported key, it cannot flow by env inheritance. resolveApiKey's
|
|
230
229
|
// precedence (env var > config file) makes both cases one code path; when the env
|
|
231
|
-
// var is set this re-writes the same value.
|
|
232
|
-
//
|
|
233
|
-
function resolveLlmKeyInputs(config: Config): Pick<DaemonEnvInputs, "anthropicApiKey"
|
|
230
|
+
// var is set this re-writes the same value. Anthropic is the only provider the
|
|
231
|
+
// local stack executes on, so it is the only key with a persisted delivery path.
|
|
232
|
+
function resolveLlmKeyInputs(config: Config): Pick<DaemonEnvInputs, "anthropicApiKey"> {
|
|
233
|
+
if (resolveProvider(config) !== "anthropic") return {};
|
|
234
234
|
const key = resolveApiKey(config);
|
|
235
|
-
|
|
236
|
-
switch (resolveProvider(config)) {
|
|
237
|
-
case "anthropic":
|
|
238
|
-
return { anthropicApiKey: key };
|
|
239
|
-
case "openai":
|
|
240
|
-
return { openaiApiKey: key };
|
|
241
|
-
default:
|
|
242
|
-
return {};
|
|
243
|
-
}
|
|
235
|
+
return key === "" ? {} : { anthropicApiKey: key };
|
|
244
236
|
}
|
|
245
237
|
|
|
246
|
-
// Default to managed Temporal unless the opaque local config explicitly opts
|
|
247
|
-
//
|
|
238
|
+
// Default to managed Temporal unless the opaque local config explicitly opts
|
|
239
|
+
// out — zero-config `stigmer up` must work on a fresh machine.
|
|
248
240
|
function isTemporalManaged(config: Config): boolean {
|
|
249
241
|
const local = config.backend.local as { temporal?: { managed?: boolean } } | undefined;
|
|
250
242
|
return local?.temporal?.managed !== false;
|
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
resolveModel,
|
|
8
8
|
resolveProvider,
|
|
9
9
|
setLlm,
|
|
10
|
-
withProviderDefaults,
|
|
11
10
|
} from "./llm-config.js";
|
|
12
11
|
|
|
13
12
|
function config(local?: unknown): Config {
|
|
@@ -15,60 +14,50 @@ function config(local?: unknown): Config {
|
|
|
15
14
|
}
|
|
16
15
|
|
|
17
16
|
describe("detectProviderFromEnv", () => {
|
|
18
|
-
it("
|
|
19
|
-
expect(detectProviderFromEnv({ ANTHROPIC_API_KEY: "a"
|
|
20
|
-
|
|
17
|
+
it("detects Anthropic only — the sole provider local execution supports", () => {
|
|
18
|
+
expect(detectProviderFromEnv({ ANTHROPIC_API_KEY: "a" })).toBe("anthropic");
|
|
19
|
+
// Other provider keys in the shell must not flip the local story.
|
|
20
|
+
expect(detectProviderFromEnv({ OPENAI_API_KEY: "b" })).toBe("");
|
|
21
21
|
expect(detectProviderFromEnv({})).toBe("");
|
|
22
22
|
});
|
|
23
23
|
});
|
|
24
24
|
|
|
25
25
|
describe("resolveProvider", () => {
|
|
26
26
|
it("honors env override > config > autodetect", () => {
|
|
27
|
-
const cfg = config({ llm: { provider: "
|
|
28
|
-
expect(resolveProvider(cfg, { STIGMER_LLM_PROVIDER: "
|
|
29
|
-
expect(resolveProvider(cfg, {})).toBe("
|
|
27
|
+
const cfg = config({ llm: { provider: "anthropic" } });
|
|
28
|
+
expect(resolveProvider(cfg, { STIGMER_LLM_PROVIDER: "other" })).toBe("other");
|
|
29
|
+
expect(resolveProvider(cfg, {})).toBe("anthropic");
|
|
30
30
|
expect(resolveProvider(config(), { ANTHROPIC_API_KEY: "x" })).toBe("anthropic");
|
|
31
31
|
expect(resolveProvider(config(), {})).toBe("");
|
|
32
32
|
});
|
|
33
33
|
});
|
|
34
34
|
|
|
35
35
|
describe("resolveModel", () => {
|
|
36
|
-
it("
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
expect(resolveModel(config({ llm: { provider: "
|
|
36
|
+
it("returns the explicit override, else empty — the registry owns the default", () => {
|
|
37
|
+
// No hardcoded model version anywhere in the CLI: unset means "platform
|
|
38
|
+
// default", resolved by the runner from the model registry.
|
|
39
|
+
expect(resolveModel(config({ llm: { provider: "anthropic" } }), {})).toBe("");
|
|
40
|
+
expect(resolveModel(config(), {})).toBe("");
|
|
40
41
|
expect(resolveModel(config({ llm: { provider: "anthropic", model: "claude-x" } }), {})).toBe("claude-x");
|
|
41
42
|
expect(resolveModel(config({ llm: { provider: "anthropic" } }), { STIGMER_LLM_MODEL: "envm" })).toBe("envm");
|
|
42
43
|
});
|
|
43
44
|
});
|
|
44
45
|
|
|
45
46
|
describe("resolveApiKey", () => {
|
|
46
|
-
it("uses
|
|
47
|
+
it("uses ANTHROPIC_API_KEY, then config", () => {
|
|
47
48
|
const cfg = config({ llm: { provider: "anthropic", api_key: "cfgkey" } });
|
|
48
49
|
expect(resolveApiKey(cfg, { ANTHROPIC_API_KEY: "envkey" })).toBe("envkey");
|
|
49
50
|
expect(resolveApiKey(cfg, {})).toBe("cfgkey");
|
|
50
51
|
});
|
|
51
|
-
});
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
expect(withProviderDefaults({ provider: "anthropic" })).toEqual({ provider: "anthropic", model: "claude-sonnet-4.5" });
|
|
56
|
-
expect(withProviderDefaults({ provider: "ollama" })).toEqual({
|
|
57
|
-
provider: "ollama",
|
|
58
|
-
model: "qwen2.5-coder:7b",
|
|
59
|
-
base_url: "http://localhost:11434",
|
|
60
|
-
});
|
|
61
|
-
expect(withProviderDefaults({ provider: "openai", api_key: "k" })).toEqual({
|
|
62
|
-
provider: "openai",
|
|
63
|
-
model: "gpt-4",
|
|
64
|
-
api_key: "k",
|
|
65
|
-
});
|
|
53
|
+
it("returns empty for non-anthropic providers (e.g. hand-edited config)", () => {
|
|
54
|
+
expect(resolveApiKey(config({ llm: { provider: "other", api_key: "k" } }), {})).toBe("");
|
|
66
55
|
});
|
|
67
56
|
});
|
|
68
57
|
|
|
69
58
|
describe("setLlm", () => {
|
|
70
59
|
it("preserves sibling local keys and can clear the section", () => {
|
|
71
|
-
const cfg = config({ temporal: { managed: true }, llm: { provider: "
|
|
60
|
+
const cfg = config({ temporal: { managed: true }, llm: { provider: "anthropic" } });
|
|
72
61
|
const updated = setLlm(cfg, { provider: "anthropic", model: "m" });
|
|
73
62
|
expect(readLlm(updated)).toEqual({ provider: "anthropic", model: "m" });
|
|
74
63
|
expect((updated.backend.local as { temporal: unknown }).temporal).toEqual({ managed: true });
|
package/src/local/llm-config.ts
CHANGED
|
@@ -2,12 +2,23 @@
|
|
|
2
2
|
// the LLM settings `setup` writes and `status` reads.
|
|
3
3
|
//
|
|
4
4
|
// The config module (config/config.ts) carries `backend.local` through verbatim
|
|
5
|
-
// so the
|
|
6
|
-
// lens* for the one sub-tree
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
5
|
+
// so the CLI never drops fields it does not model. This module adds a *typed
|
|
6
|
+
// lens* for the one sub-tree setup owns — `local.llm` — and resolves the
|
|
7
|
+
// effective provider/model with env-override precedence (env > config file >
|
|
8
|
+
// API-key autodetect). Writes merge into a shallow copy of `local`, preserving
|
|
9
|
+
// every sibling key (temporal, execution, …).
|
|
10
|
+
//
|
|
11
|
+
// Local agent execution is Anthropic-only: the native runner constructs
|
|
12
|
+
// Anthropic clients, and the platform model registry's native-harness entries
|
|
13
|
+
// are all Anthropic models. Two invariants follow:
|
|
14
|
+
//
|
|
15
|
+
// 1. The model registry — not this config — owns the default execution model.
|
|
16
|
+
// The runner resolves it from the registry at execution time, so this
|
|
17
|
+
// module never hardcodes a model version (hardcoded versions here drifted
|
|
18
|
+
// from the registry repeatedly). An unset model means "platform default".
|
|
19
|
+
// 2. `provider` exists to route the right API key to the runner, not to
|
|
20
|
+
// select an execution backend; "anthropic" is the only provider the local
|
|
21
|
+
// stack can serve.
|
|
11
22
|
|
|
12
23
|
import type { Config } from "../config/config.js";
|
|
13
24
|
|
|
@@ -16,7 +27,6 @@ export interface LlmSettings {
|
|
|
16
27
|
provider?: string;
|
|
17
28
|
model?: string;
|
|
18
29
|
api_key?: string;
|
|
19
|
-
base_url?: string;
|
|
20
30
|
}
|
|
21
31
|
|
|
22
32
|
interface LocalSection {
|
|
@@ -24,18 +34,6 @@ interface LocalSection {
|
|
|
24
34
|
[key: string]: unknown;
|
|
25
35
|
}
|
|
26
36
|
|
|
27
|
-
/** Default model per provider, matching the Go setup wizard's choices. */
|
|
28
|
-
const DEFAULT_MODEL: Record<string, string> = {
|
|
29
|
-
ollama: "qwen2.5-coder:7b",
|
|
30
|
-
anthropic: "claude-sonnet-4.5",
|
|
31
|
-
openai: "gpt-4",
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
/** Default base URL per provider (only Ollama needs one). */
|
|
35
|
-
const DEFAULT_BASE_URL: Record<string, string> = {
|
|
36
|
-
ollama: "http://localhost:11434",
|
|
37
|
-
};
|
|
38
|
-
|
|
39
37
|
function localSection(config: Config): LocalSection {
|
|
40
38
|
const local = config.backend.local;
|
|
41
39
|
return local !== null && typeof local === "object" ? (local as LocalSection) : {};
|
|
@@ -47,12 +45,11 @@ export function readLlm(config: Config): LlmSettings | undefined {
|
|
|
47
45
|
}
|
|
48
46
|
|
|
49
47
|
/**
|
|
50
|
-
* Detect a provider from API keys present in the environment
|
|
51
|
-
*
|
|
48
|
+
* Detect a provider from API keys present in the environment. Anthropic is the
|
|
49
|
+
* only provider local execution supports, so only ANTHROPIC_API_KEY counts.
|
|
52
50
|
*/
|
|
53
51
|
export function detectProviderFromEnv(env: NodeJS.ProcessEnv = process.env): string {
|
|
54
52
|
if (env.ANTHROPIC_API_KEY) return "anthropic";
|
|
55
|
-
if (env.OPENAI_API_KEY) return "openai";
|
|
56
53
|
return "";
|
|
57
54
|
}
|
|
58
55
|
|
|
@@ -64,33 +61,22 @@ export function resolveProvider(config: Config, env: NodeJS.ProcessEnv = process
|
|
|
64
61
|
return detectProviderFromEnv(env);
|
|
65
62
|
}
|
|
66
63
|
|
|
67
|
-
/**
|
|
64
|
+
/**
|
|
65
|
+
* Effective model override: env override > config. Empty string means "no
|
|
66
|
+
* override" — the runner picks the default from the platform model registry.
|
|
67
|
+
*/
|
|
68
68
|
export function resolveModel(config: Config, env: NodeJS.ProcessEnv = process.env): string {
|
|
69
69
|
if (env.STIGMER_LLM_MODEL) return env.STIGMER_LLM_MODEL;
|
|
70
|
-
|
|
71
|
-
if (llm?.model) return llm.model;
|
|
72
|
-
return DEFAULT_MODEL[resolveProvider(config, env)] ?? "";
|
|
70
|
+
return readLlm(config)?.model ?? "";
|
|
73
71
|
}
|
|
74
72
|
|
|
75
|
-
/** Effective API key:
|
|
73
|
+
/** Effective API key: ANTHROPIC_API_KEY env var > config. */
|
|
76
74
|
export function resolveApiKey(config: Config, env: NodeJS.ProcessEnv = process.env): string {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if (envKey) return envKey;
|
|
75
|
+
if (resolveProvider(config, env) !== "anthropic") return "";
|
|
76
|
+
if (env.ANTHROPIC_API_KEY) return env.ANTHROPIC_API_KEY;
|
|
80
77
|
return readLlm(config)?.api_key ?? "";
|
|
81
78
|
}
|
|
82
79
|
|
|
83
|
-
/** Fill in provider-appropriate defaults for a partial selection. */
|
|
84
|
-
export function withProviderDefaults(settings: LlmSettings): LlmSettings {
|
|
85
|
-
const provider = settings.provider ?? "";
|
|
86
|
-
const out: LlmSettings = { provider };
|
|
87
|
-
out.model = settings.model ?? DEFAULT_MODEL[provider];
|
|
88
|
-
if (settings.api_key) out.api_key = settings.api_key;
|
|
89
|
-
const baseUrl = settings.base_url ?? DEFAULT_BASE_URL[provider];
|
|
90
|
-
if (baseUrl) out.base_url = baseUrl;
|
|
91
|
-
return out;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
80
|
/**
|
|
95
81
|
* Return a copy of `config` with the LLM section replaced, preserving every
|
|
96
82
|
* other key under `backend.local`. Passing `undefined` clears the LLM section
|
|
@@ -1,25 +1,33 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
import type { Config } from "../../config/config.js";
|
|
3
3
|
import { readLlm } from "../llm-config.js";
|
|
4
|
-
import { applyChoice, buildLlmForChoice } from "./wizard.js";
|
|
4
|
+
import { PROVIDER_CHOICES, applyChoice, buildLlmForChoice } from "./wizard.js";
|
|
5
5
|
|
|
6
6
|
function config(local?: unknown): Config {
|
|
7
7
|
return { backend: { type: "local", local } };
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
// Regression pin: every provider offered by `stigmer setup` promises an
|
|
11
|
+
// execution path the native runner must actually serve. The runner constructs
|
|
12
|
+
// Anthropic clients only and the registry's native-harness entries are all
|
|
13
|
+
// Anthropic — so this list is anthropic + skip, full stop. If this test fails
|
|
14
|
+
// because a provider was added, the native runner and model registry must gain
|
|
15
|
+
// real support for it first (see sdk/react DISABLED_PROVIDERS for the same
|
|
16
|
+
// decision on the UI side).
|
|
17
|
+
describe("PROVIDER_CHOICES", () => {
|
|
18
|
+
it("offers exactly anthropic and skip", () => {
|
|
19
|
+
expect([...PROVIDER_CHOICES]).toEqual(["anthropic", "skip"]);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
10
23
|
describe("buildLlmForChoice", () => {
|
|
11
|
-
it("
|
|
12
|
-
expect(buildLlmForChoice("anthropic")).toEqual({ provider: "anthropic"
|
|
24
|
+
it("persists no model unless explicitly overridden — the registry owns the default", () => {
|
|
25
|
+
expect(buildLlmForChoice("anthropic")).toEqual({ provider: "anthropic" });
|
|
13
26
|
expect(buildLlmForChoice("anthropic", { apiKey: "k", model: "m" })).toEqual({
|
|
14
27
|
provider: "anthropic",
|
|
15
28
|
model: "m",
|
|
16
29
|
api_key: "k",
|
|
17
30
|
});
|
|
18
|
-
expect(buildLlmForChoice("ollama", { baseUrl: "http://host:1" })).toEqual({
|
|
19
|
-
provider: "ollama",
|
|
20
|
-
model: "qwen2.5-coder:7b",
|
|
21
|
-
base_url: "http://host:1",
|
|
22
|
-
});
|
|
23
31
|
});
|
|
24
32
|
|
|
25
33
|
it("returns undefined for skip (clears config)", () => {
|
|
@@ -29,14 +37,14 @@ describe("buildLlmForChoice", () => {
|
|
|
29
37
|
|
|
30
38
|
describe("applyChoice", () => {
|
|
31
39
|
it("writes the selection while preserving siblings", () => {
|
|
32
|
-
const cfg = config({ temporal: { managed: true }, llm: { provider: "
|
|
40
|
+
const cfg = config({ temporal: { managed: true }, llm: { provider: "anthropic", model: "stale" } });
|
|
33
41
|
const updated = applyChoice(cfg, "anthropic", { apiKey: "k" });
|
|
34
|
-
expect(readLlm(updated)).toEqual({ provider: "anthropic",
|
|
42
|
+
expect(readLlm(updated)).toEqual({ provider: "anthropic", api_key: "k" });
|
|
35
43
|
expect((updated.backend.local as { temporal: unknown }).temporal).toEqual({ managed: true });
|
|
36
44
|
});
|
|
37
45
|
|
|
38
46
|
it("skip clears the llm section", () => {
|
|
39
|
-
const updated = applyChoice(config({ llm: { provider: "
|
|
47
|
+
const updated = applyChoice(config({ llm: { provider: "anthropic" } }), "skip");
|
|
40
48
|
expect(readLlm(updated)).toBeUndefined();
|
|
41
49
|
});
|
|
42
50
|
});
|