diff --git a/docs-site/content/docs/cli-reference/ktx-setup.mdx b/docs-site/content/docs/cli-reference/ktx-setup.mdx index 8ae4469d..f5b5cf26 100644 --- a/docs-site/content/docs/cli-reference/ktx-setup.mdx +++ b/docs-site/content/docs/cli-reference/ktx-setup.mdx @@ -60,6 +60,11 @@ prompts. | `--vertex-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 ` 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 | diff --git a/packages/cli/src/commands/setup-commands.ts b/packages/cli/src/commands/setup-commands.ts index 418b27f9..a51ac72e 100644 --- a/packages/cli/src/commands/setup-commands.ts +++ b/packages/cli/src/commands/setup-commands.ts @@ -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 ', 'LLM backend').argParser(llmBackend).hideHelp()) + .addOption( + new Option('--llm-backend ', 'LLM backend') + .choices(['anthropic', 'vertex', 'claude-code', 'codex']) + .hideHelp(), + ) .addOption( new Option('--anthropic-api-key-env ', 'Environment variable containing the Anthropic API key').hideHelp(), ) @@ -230,7 +220,11 @@ export function registerSetupCommands(program: Command, context: KtxCliCommandCo .addOption(new Option('--vertex-project ', 'Google Vertex AI project ID, env:NAME, or file:/path').hideHelp()) .addOption(new Option('--vertex-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 ', 'Embedding backend').argParser(embeddingBackend).hideHelp()) + .addOption( + new Option('--embedding-backend ', 'Embedding backend') + .choices(['openai', 'sentence-transformers']) + .hideHelp(), + ) .addOption( new Option( '--embedding-api-key-env ', diff --git a/packages/cli/src/setup-embeddings.ts b/packages/cli/src/setup-embeddings.ts index 5d02e3e4..92be5d25 100644 --- a/packages/cli/src/setup-embeddings.ts +++ b/packages/cli/src/setup-embeddings.ts @@ -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' }; } diff --git a/packages/cli/src/setup-models.ts b/packages/cli/src/setup-models.ts index 911579a9..890b81ef 100644 --- a/packages/cli/src/setup-models.ts +++ b/packages/cli/src/setup-models.ts @@ -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(); diff --git a/packages/cli/test/index.test.ts b/packages/cli/test/index.test.ts index eef511fb..c0c285ca 100644 --- a/packages/cli/test/index.test.ts +++ b/packages/cli/test/index.test.ts @@ -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 () => { diff --git a/packages/cli/test/setup-models.test.ts b/packages/cli/test/setup-models.test.ts index f09691e0..d79287a7 100644 --- a/packages/cli/test/setup-models.test.ts +++ b/packages/cli/test/setup-models.test.ts @@ -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 () => {