mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-22 08:38:08 +02:00
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:
parent
48676c74fa
commit
41acc5959c
6 changed files with 67 additions and 26 deletions
|
|
@ -60,6 +60,11 @@ prompts.
|
|||
| `--vertex-location <location>` | Vertex AI location, `env:NAME`, or `file:/path` reference |
|
||||
| `--skip-llm` | Leave LLM setup incomplete |
|
||||
|
||||
Non-interactive setup (`--no-input`) requires an explicit `--llm-backend`; it
|
||||
does not assume a default. `--llm-backend` is independent of `--target`: the
|
||||
target selects which agent client receives integration, while `--llm-backend`
|
||||
selects the provider **ktx** itself uses.
|
||||
|
||||
Choose only one Anthropic credential source. Anthropic credential flags are only
|
||||
valid with the Anthropic backend; Vertex flags are only valid with the Vertex
|
||||
backend. The `claude-code` and `codex` backends use local authentication instead
|
||||
|
|
@ -276,6 +281,7 @@ Use `ktx status` for repeatable readiness checks after setup exits.
|
|||
|-------|-------|----------|
|
||||
| Setup resumes an unexpected project | `KTX_PROJECT_DIR` or nearest `ktx.yaml` points to another directory | Pass `--project-dir <path>` explicitly |
|
||||
| Setup cannot run in CI | Required values are missing and `--no-input` disables prompts | Provide the relevant automation flags or create a fixture `ktx.yaml` |
|
||||
| `Missing LLM backend` | `--no-input` without `--llm-backend` | Pass `--llm-backend` with `anthropic`, `vertex`, `claude-code`, or `codex` (the latter two need no API key) |
|
||||
| Provider health check fails | Provider key, model id, Vertex project, or Vertex location is invalid | Fix the `env:` or `file:` reference and rerun setup |
|
||||
| Python runtime is missing | The selected setup needs runtime-backed agent, query-history, Looker, or local embedding features | Accept the interactive prompt, rerun with `--yes`, or run the suggested `ktx admin runtime install` command |
|
||||
| `--enable-query-history` is rejected | The selected database driver does not support query history | Use Postgres, BigQuery, or Snowflake, or rerun without query-history flags |
|
||||
|
|
|
|||
|
|
@ -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>',
|
||||
|
|
|
|||
|
|
@ -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' };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -1522,7 +1522,8 @@ describe('runKtxCli', () => {
|
|||
).resolves.toBe(1);
|
||||
|
||||
expect(setup).not.toHaveBeenCalled();
|
||||
expect(setupIo.stderr()).toContain("invalid choice 'deterministic'");
|
||||
expect(setupIo.stderr()).toContain("argument 'deterministic' is invalid");
|
||||
expect(setupIo.stderr()).toContain('Allowed choices are openai, sentence-transformers');
|
||||
});
|
||||
|
||||
it('rejects gateway as a setup embedding backend', async () => {
|
||||
|
|
@ -1534,7 +1535,8 @@ describe('runKtxCli', () => {
|
|||
).resolves.toBe(1);
|
||||
|
||||
expect(setup).not.toHaveBeenCalled();
|
||||
expect(setupIo.stderr()).toContain("invalid choice 'gateway'");
|
||||
expect(setupIo.stderr()).toContain("argument 'gateway' is invalid");
|
||||
expect(setupIo.stderr()).toContain('Allowed choices are openai, sentence-transformers');
|
||||
});
|
||||
|
||||
it('rejects conflicting embedding credential setup flags', async () => {
|
||||
|
|
|
|||
|
|
@ -814,7 +814,7 @@ describe('setup Anthropic model step', () => {
|
|||
expect(io.stderr()).toContain(`Missing Anthropic API key file: ${missingSecretPath}`);
|
||||
});
|
||||
|
||||
it('does not recommend skipping when non-interactive setup is missing an Anthropic credential source', async () => {
|
||||
it('requires an explicit LLM backend in non-interactive setup instead of silently defaulting to anthropic', async () => {
|
||||
const io = makeIo();
|
||||
|
||||
const result = await runKtxSetupAnthropicModelStep(
|
||||
|
|
@ -823,10 +823,33 @@ describe('setup Anthropic model step', () => {
|
|||
);
|
||||
|
||||
expect(result.status).toBe('missing-input');
|
||||
expect(io.stderr()).toContain(
|
||||
'Missing Anthropic API key: pass --anthropic-api-key-env or --anthropic-api-key-file.',
|
||||
const err = io.stderr();
|
||||
// Names the flag the user actually needs and lists every valid backend.
|
||||
expect(err).toContain('--llm-backend');
|
||||
expect(err).toContain('anthropic');
|
||||
expect(err).toContain('vertex');
|
||||
expect(err).toContain('claude-code');
|
||||
expect(err).toContain('codex');
|
||||
// No silent anthropic default, so the bare api-key error is not the reason shown.
|
||||
expect(err).not.toContain('Missing Anthropic API key: pass --anthropic-api-key-env');
|
||||
expect(err).not.toContain('--skip-llm');
|
||||
});
|
||||
|
||||
it('names --llm-backend when an explicit anthropic backend is missing its API key in non-interactive setup', async () => {
|
||||
const io = makeIo();
|
||||
|
||||
const result = await runKtxSetupAnthropicModelStep(
|
||||
{ projectDir: tempDir, inputMode: 'disabled', llmBackend: 'anthropic', skipLlm: false },
|
||||
io.io,
|
||||
);
|
||||
expect(io.stderr()).not.toContain('--skip-llm');
|
||||
|
||||
expect(result.status).toBe('missing-input');
|
||||
const err = io.stderr();
|
||||
expect(err).toContain('--anthropic-api-key-env');
|
||||
// Reveals that the backend itself is selectable, so a user who wanted a keyless
|
||||
// backend (claude-code/codex) can discover it from the error.
|
||||
expect(err).toContain('--llm-backend');
|
||||
expect(err).not.toContain('--skip-llm');
|
||||
});
|
||||
|
||||
it('writes pasted keys to .ktx/secrets and never prints the key', async () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue