fix(setup): require explicit --llm-backend in non-interactive setup

Non-interactive setup (--no-input) silently defaulted the LLM backend to
anthropic, the one backend that cannot self-configure without an extra flag,
then failed with 'Missing Anthropic API key: pass --anthropic-api-key-env or
--anthropic-api-key-file.' — an error that never mentioned --llm-backend. A
user who passed --target claude-code had no way to discover the (hidden)
--llm-backend claude-code flag from the error.

- chooseBackend no longer silently picks anthropic in disabled mode; it fails
  with a message naming --llm-backend, listing every backend, and noting that
  claude-code/codex use local auth (no key).
- The anthropic and embedding credential errors now name --llm-backend /
  --embedding-backend so a keyless backend is discoverable from the error.
- --llm-backend and --embedding-backend use .choices(), so invalid values
  report the allowed set (and the bespoke parser fns are removed).

Only invocations that already failed change behavior; they now fail with an
actionable error instead of a cryptic one.
This commit is contained in:
Andrey Avtomonov 2026-06-09 12:06:05 +02:00
parent 48676c74fa
commit 41acc5959c
6 changed files with 67 additions and 26 deletions

View file

@ -21,20 +21,6 @@ function positiveInteger(value: string): number {
return parsed;
}
function embeddingBackend(value: string): 'openai' | 'sentence-transformers' {
if (value === 'openai' || value === 'sentence-transformers') {
return value;
}
throw new InvalidArgumentError(`invalid choice '${value}'`);
}
function llmBackend(value: string): KtxSetupLlmBackend {
if (value === 'anthropic' || value === 'vertex' || value === 'claude-code' || value === 'codex') {
return value;
}
throw new InvalidArgumentError(`invalid choice '${value}'`);
}
function databaseDriver(value: string): KtxSetupDatabaseDriver {
if (
value === 'sqlite' ||
@ -220,7 +206,11 @@ export function registerSetupCommands(program: Command, context: KtxCliCommandCo
.addOption(new Option('--skip-agents', 'Leave agent integration incomplete for now').hideHelp().default(false))
.option('--yes', 'Accept project creation and runtime install defaults where setup confirms', false)
.option('--no-input', 'Disable interactive terminal input')
.addOption(new Option('--llm-backend <backend>', 'LLM backend').argParser(llmBackend).hideHelp())
.addOption(
new Option('--llm-backend <backend>', 'LLM backend')
.choices(['anthropic', 'vertex', 'claude-code', 'codex'])
.hideHelp(),
)
.addOption(
new Option('--anthropic-api-key-env <name>', 'Environment variable containing the Anthropic API key').hideHelp(),
)
@ -230,7 +220,11 @@ export function registerSetupCommands(program: Command, context: KtxCliCommandCo
.addOption(new Option('--vertex-project <project>', 'Google Vertex AI project ID, env:NAME, or file:/path').hideHelp())
.addOption(new Option('--vertex-location <location>', 'Google Vertex AI location, env:NAME, or file:/path').hideHelp())
.addOption(new Option('--skip-llm', 'Leave LLM setup incomplete for now').hideHelp().default(false))
.addOption(new Option('--embedding-backend <backend>', 'Embedding backend').argParser(embeddingBackend).hideHelp())
.addOption(
new Option('--embedding-backend <backend>', 'Embedding backend')
.choices(['openai', 'sentence-transformers'])
.hideHelp(),
)
.addOption(
new Option(
'--embedding-api-key-env <name>',

View file

@ -213,7 +213,10 @@ async function chooseCredentialRef(
return { status: 'ready', ref, value };
}
if (args.inputMode === 'disabled') {
io.stderr.write('Missing embedding API key: pass --embedding-api-key-env or --embedding-api-key-file.\n');
io.stderr.write(
'Missing embedding API key for --embedding-backend openai: pass --embedding-api-key-env or --embedding-api-key-file ' +
'(or use --embedding-backend sentence-transformers for local embeddings).\n',
);
return { status: 'missing-input' };
}

View file

@ -135,7 +135,8 @@ const execFileAsync = promisify(execFile);
type ChooseBackendResult =
| { status: 'ready'; backend: KtxSetupLlmBackend; prompted: boolean }
| { status: 'back' };
| { status: 'back' }
| { status: 'missing-input' };
type VertexConfigChoice =
| {
@ -372,7 +373,10 @@ async function chooseCredentialRef(
return { status: 'ready', ref, value };
}
if (args.inputMode === 'disabled') {
io.stderr.write('Missing Anthropic API key: pass --anthropic-api-key-env or --anthropic-api-key-file.\n');
io.stderr.write(
'Missing Anthropic API key for --llm-backend anthropic: pass --anthropic-api-key-env or --anthropic-api-key-file ' +
'(or use --llm-backend claude-code or --llm-backend codex for local subscription auth).\n',
);
return { status: 'missing-input' };
}
@ -446,7 +450,16 @@ async function chooseBackend(
return { status: 'ready', backend: explicit, prompted: false };
}
if (args.inputMode === 'disabled') {
return { status: 'ready', backend: 'anthropic', prompted: false };
// No safe default exists: anthropic/vertex need credentials and claude-code/codex
// need local auth, so non-interactive setup must be told which backend to use rather
// than silently picking one that cannot self-configure.
io.stderr.write(
'Missing LLM backend: pass --llm-backend with one of anthropic, vertex, claude-code, codex.\n' +
' claude-code, codex — use your local subscription auth (no API key)\n' +
' anthropic — also pass --anthropic-api-key-env or --anthropic-api-key-file\n' +
' vertex — also pass --vertex-project (and optionally --vertex-location)\n',
);
return { status: 'missing-input' };
}
const prompts = deps.prompts ?? createPromptAdapter();