mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-10 08:05:14 +02:00
feat: support claude-code setup and status
This commit is contained in:
parent
418a8e17ae
commit
ade9b4a5db
14 changed files with 288 additions and 51 deletions
|
|
@ -51,7 +51,8 @@ scripted project creation. They are not shown in `ktx setup --help`.
|
|||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--llm-backend <backend>` | LLM backend: `anthropic` or `vertex` |
|
||||
| `--llm-backend <backend>` | LLM backend: `anthropic`, `vertex`, or `claude-code` |
|
||||
| `--llm-backend claude-code` | Use the local Claude Code session for KTX LLM calls |
|
||||
| `--anthropic-api-key-env <name>` | Environment variable containing the Anthropic API key |
|
||||
| `--anthropic-api-key-file <path>` | File containing the Anthropic API key |
|
||||
| `--anthropic-model <model>` | Anthropic model ID to validate and save |
|
||||
|
|
@ -61,7 +62,8 @@ scripted project creation. They are not shown in `ktx setup --help`.
|
|||
|
||||
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.
|
||||
backend. The `claude-code` backend uses local Claude Code authentication instead
|
||||
of Anthropic API key or Vertex flags.
|
||||
|
||||
### Embeddings
|
||||
|
||||
|
|
|
|||
|
|
@ -47,6 +47,10 @@ ktx status --project-dir ./analytics
|
|||
`ktx status` prints grouped doctor checks. Agents should use
|
||||
`ktx status --json --no-input` when they need to branch on readiness state.
|
||||
|
||||
For `llm.provider.backend: claude-code`, `ktx status` checks that the local
|
||||
Claude Code session is usable. If auth fails, run the Claude Code CLI login
|
||||
flow, then rerun `ktx status`.
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "KTX project doctor",
|
||||
|
|
|
|||
|
|
@ -59,12 +59,13 @@ completed setup steps and resumes from the remaining work.
|
|||
KTX uses a Claude model for ingest agents that turn schemas, SQL, BI metadata,
|
||||
and documents into semantic-layer sources and wiki context.
|
||||
|
||||
Setup supports two LLM provider paths:
|
||||
Setup supports three LLM provider paths:
|
||||
|
||||
| Provider | Use when | Credential model |
|
||||
|----------|----------|------------------|
|
||||
| Anthropic API | You have an Anthropic API key | `ANTHROPIC_API_KEY` or a local `file:` secret |
|
||||
| Google Vertex AI for Anthropic Claude | Your organization runs Claude through Google Cloud | Application Default Credentials plus Vertex project and location |
|
||||
| Claude Code | You want KTX to use your local Claude Code session | Claude Code local authentication |
|
||||
|
||||
For Anthropic API, setup can read the key from the environment or save a pasted
|
||||
key to `.ktx/secrets/anthropic-api-key`. `ktx.yaml` stores an `env:` or `file:`
|
||||
|
|
@ -74,6 +75,25 @@ For Vertex AI, setup uses Google Application Default Credentials. It can read
|
|||
your active `gcloud` project, list visible projects, or accept explicit
|
||||
`--vertex-project` and `--vertex-location` values.
|
||||
|
||||
To use your local Claude Code session instead of an API key, set:
|
||||
|
||||
```yaml
|
||||
llm:
|
||||
provider:
|
||||
backend: claude-code
|
||||
models:
|
||||
default: sonnet
|
||||
triage: haiku
|
||||
candidateExtraction: sonnet
|
||||
curator: sonnet
|
||||
reconcile: sonnet
|
||||
repair: sonnet
|
||||
```
|
||||
|
||||
`claude-code` uses the Claude Code authentication already configured on your
|
||||
machine. It doesn't use `ANTHROPIC_API_KEY`, Vertex credentials, AI Gateway
|
||||
tokens, or Bedrock credentials.
|
||||
|
||||
Setup checks the selected model before saving. Anthropic API setup fetches live
|
||||
Claude model choices when possible and falls back to bundled defaults if model
|
||||
discovery is unavailable.
|
||||
|
|
|
|||
|
|
@ -58,6 +58,10 @@ ktx ingest --all --deep
|
|||
Deep ingest needs LLM and embedding readiness. If those providers are not
|
||||
configured, run `ktx setup` or use `--fast`.
|
||||
|
||||
When you use `claude-code`, KTX still controls the tool surface for ingest and
|
||||
memory capture. Claude Code built-in tools, discovered MCP servers, hooks,
|
||||
skills, plugins, agents, and slash commands are not exposed to KTX agent loops.
|
||||
|
||||
## Query history
|
||||
|
||||
PostgreSQL, BigQuery, and Snowflake can add query-history context. This helps
|
||||
|
|
|
|||
48
docs-site/content/docs/guides/llm-configuration.mdx
Normal file
48
docs-site/content/docs/guides/llm-configuration.mdx
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
---
|
||||
title: LLM configuration
|
||||
description: Configure KTX LLM providers, model roles, and prompt caching.
|
||||
---
|
||||
|
||||
KTX uses the top-level `llm` block in `ktx.yaml` for text generation,
|
||||
structured extraction, and ingest or memory agent loops.
|
||||
|
||||
## Backends
|
||||
|
||||
Set `llm.provider.backend` to one of these values:
|
||||
|
||||
- `anthropic`: Use the Anthropic API through `ANTHROPIC_API_KEY` or the
|
||||
configured `api_key` reference.
|
||||
- `vertex`: Use Vertex AI Anthropic models through Google Cloud credentials.
|
||||
- `gateway`: Use AI Gateway-compatible Anthropic model ids.
|
||||
- `claude-code`: Use your local Claude Code session through the Claude Agent
|
||||
SDK. KTX removes provider-routing environment variables from Claude Code
|
||||
child processes, so this backend doesn't silently fall back to
|
||||
`ANTHROPIC_API_KEY`, Vertex, Gateway, or Bedrock credentials.
|
||||
|
||||
## Claude Code
|
||||
|
||||
Use aliases or full Claude model ids in `llm.models`:
|
||||
|
||||
```yaml
|
||||
llm:
|
||||
provider:
|
||||
backend: claude-code
|
||||
models:
|
||||
default: sonnet
|
||||
triage: haiku
|
||||
candidateExtraction: sonnet
|
||||
curator: sonnet
|
||||
reconcile: sonnet
|
||||
repair: sonnet
|
||||
```
|
||||
|
||||
`claude-code` keeps KTX tool boundaries intact. KTX exposes only the MCP tools
|
||||
needed for the current KTX agent loop and disables Claude Code built-in tools,
|
||||
filesystem settings, skills, plugins, agents, hooks, and slash commands.
|
||||
|
||||
## Prompt caching
|
||||
|
||||
`llm.promptCaching` has partial parity on `claude-code`. KTX doesn't pass
|
||||
Anthropic cache-control markers to the Claude Agent SDK. Status and doctor warn
|
||||
when you configure prompt-cache TTL, tool, or history fields that the Claude
|
||||
Agent SDK backend ignores.
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"title": "Guides",
|
||||
"defaultOpen": true,
|
||||
"pages": ["building-context", "writing-context", "serving-agents"]
|
||||
"pages": ["building-context", "llm-configuration", "writing-context", "serving-agents"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,12 @@ const config = {
|
|||
},
|
||||
async redirects() {
|
||||
return [
|
||||
{
|
||||
source: "/docs",
|
||||
destination: "/docs/getting-started/introduction",
|
||||
permanent: false,
|
||||
basePath: false,
|
||||
},
|
||||
{
|
||||
source: "/:path*",
|
||||
has: [{ type: "host", value: "docs.ktx.sh" }],
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ function embeddingBackend(value: string): 'openai' | 'sentence-transformers' {
|
|||
}
|
||||
|
||||
function llmBackend(value: string): KtxSetupLlmBackend {
|
||||
if (value === 'anthropic' || value === 'vertex') {
|
||||
if (value === 'anthropic' || value === 'vertex' || value === 'claude-code') {
|
||||
return value;
|
||||
}
|
||||
throw new InvalidArgumentError(`invalid choice '${value}'`);
|
||||
|
|
@ -361,12 +361,16 @@ export function registerSetupCommands(program: Command, context: KtxCliCommandCo
|
|||
context.setExitCode(1);
|
||||
return;
|
||||
}
|
||||
if (options.llmBackend === 'vertex' && (options.anthropicApiKeyEnv || options.anthropicApiKeyFile)) {
|
||||
if (
|
||||
options.llmBackend &&
|
||||
options.llmBackend !== 'anthropic' &&
|
||||
(options.anthropicApiKeyEnv || options.anthropicApiKeyFile)
|
||||
) {
|
||||
context.io.stderr.write('Anthropic API key flags are only valid with --llm-backend anthropic.\n');
|
||||
context.setExitCode(1);
|
||||
return;
|
||||
}
|
||||
if (options.llmBackend === 'anthropic' && (options.vertexProject || options.vertexLocation)) {
|
||||
if (options.llmBackend && options.llmBackend !== 'vertex' && (options.vertexProject || options.vertexLocation)) {
|
||||
context.io.stderr.write('Vertex AI flags are only valid with --llm-backend vertex.\n');
|
||||
context.setExitCode(1);
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -464,6 +464,44 @@ describe('runKtxDoctor', () => {
|
|||
delete process.env.OPENAI_API_KEY;
|
||||
});
|
||||
|
||||
it('reports Claude Code auth failures and ignored prompt-caching fields in project doctor output', async () => {
|
||||
await writeFile(
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'llm:',
|
||||
' provider:',
|
||||
' backend: claude-code',
|
||||
' models:',
|
||||
' default: sonnet',
|
||||
' promptCaching:',
|
||||
' enabled: true',
|
||||
' systemTtl: 1h',
|
||||
' toolsTtl: 1h',
|
||||
' historyTtl: 5m',
|
||||
'',
|
||||
].join('\n'),
|
||||
'utf-8',
|
||||
);
|
||||
const testIo = makeIo();
|
||||
|
||||
await expect(
|
||||
runKtxDoctor(
|
||||
{ command: 'project', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' },
|
||||
testIo.io,
|
||||
{
|
||||
claudeCodeAuthProbe: async () => ({
|
||||
ok: false as const,
|
||||
message: 'Authenticate Claude Code locally.',
|
||||
}),
|
||||
},
|
||||
),
|
||||
).resolves.toBe(1);
|
||||
|
||||
expect(testIo.stdout()).toContain('claude-code');
|
||||
expect(testIo.stdout()).toContain('Authenticate Claude Code locally');
|
||||
expect(testIo.stdout()).toContain('claude-code ignores llm.promptCaching');
|
||||
});
|
||||
|
||||
it('includes Postgres query-history readiness in project doctor output', async () => {
|
||||
process.env.ANTHROPIC_API_KEY = 'test-key'; // pragma: allowlist secret
|
||||
process.env.OPENAI_API_KEY = 'test-key'; // pragma: allowlist secret
|
||||
|
|
|
|||
|
|
@ -265,36 +265,30 @@ export class CliLookerSlWritingAgentRunner extends AgentRunnerService {
|
|||
if (!ledger?.execute) {
|
||||
throw new Error('record_verification_ledger tool was not available to the Looker WorkUnit');
|
||||
}
|
||||
await ledger.execute(
|
||||
{
|
||||
summary: 'Test fixture verified Looker explore target identifiers before writing SL.',
|
||||
verifiedIdentifiers: ['prod-warehouse', 'public.orders'],
|
||||
unverifiedIdentifiers: [],
|
||||
},
|
||||
{ toolCallId: 'cli-looker-verification-ledger', messages: [] },
|
||||
);
|
||||
await ledger.execute({
|
||||
summary: 'Test fixture verified Looker explore target identifiers before writing SL.',
|
||||
verifiedIdentifiers: ['prod-warehouse', 'public.orders'],
|
||||
unverifiedIdentifiers: [],
|
||||
});
|
||||
const slWrite = params.toolSet.sl_write_source;
|
||||
if (!slWrite?.execute) {
|
||||
throw new Error('sl_write_source tool was not available to the Looker WorkUnit');
|
||||
}
|
||||
const result = await slWrite.execute(
|
||||
{
|
||||
connectionId: 'prod-warehouse',
|
||||
sourceName: 'looker__ecommerce__orders',
|
||||
source: {
|
||||
name: 'looker__ecommerce__orders',
|
||||
table: 'public.orders',
|
||||
grain: ['id'],
|
||||
columns: [
|
||||
{ name: 'id', type: 'number' },
|
||||
{ name: 'revenue', type: 'number' },
|
||||
],
|
||||
measures: [{ name: 'total_revenue', expr: 'sum(revenue)' }],
|
||||
},
|
||||
const result = await slWrite.execute({
|
||||
connectionId: 'prod-warehouse',
|
||||
sourceName: 'looker__ecommerce__orders',
|
||||
source: {
|
||||
name: 'looker__ecommerce__orders',
|
||||
table: 'public.orders',
|
||||
grain: ['id'],
|
||||
columns: [
|
||||
{ name: 'id', type: 'number' },
|
||||
{ name: 'revenue', type: 'number' },
|
||||
],
|
||||
measures: [{ name: 'total_revenue', expr: 'sum(revenue)' }],
|
||||
},
|
||||
{ toolCallId: 'cli-looker-sl-write', messages: [] },
|
||||
);
|
||||
if (!result.structured.success) {
|
||||
});
|
||||
if (!(result.structured as { success?: boolean } | undefined)?.success) {
|
||||
throw new Error(result.markdown);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -86,11 +86,11 @@ export interface KtxIngestDeps {
|
|||
renderStoredMemoryFlow?: typeof renderMemoryFlowTui;
|
||||
startLiveMemoryFlow?: typeof startLiveMemoryFlowTui;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
localIngestOptions?: Pick<
|
||||
RunLocalIngestOptions,
|
||||
| 'agentRunner'
|
||||
| 'llmProvider'
|
||||
| 'memoryModel'
|
||||
localIngestOptions?: Pick<
|
||||
RunLocalIngestOptions,
|
||||
| 'agentRunner'
|
||||
| 'llmRuntime'
|
||||
| 'memoryModel'
|
||||
| 'semanticLayerCompute'
|
||||
| 'queryExecutor'
|
||||
| 'logger'
|
||||
|
|
|
|||
|
|
@ -61,7 +61,12 @@ function makePromptAdapter(options: {
|
|||
if (message.includes('LLM provider')) {
|
||||
providerPromptCount += 1;
|
||||
const nextProviderChoice = selectValues[0];
|
||||
if (nextProviderChoice === 'anthropic' || nextProviderChoice === 'vertex' || nextProviderChoice === 'back') {
|
||||
if (
|
||||
nextProviderChoice === 'anthropic' ||
|
||||
nextProviderChoice === 'vertex' ||
|
||||
nextProviderChoice === 'claude-code' ||
|
||||
nextProviderChoice === 'back'
|
||||
) {
|
||||
return selectValues.shift() ?? nextProviderChoice;
|
||||
}
|
||||
if (options.credentialChoice === 'back' && providerPromptCount > 1) {
|
||||
|
|
@ -180,6 +185,30 @@ describe('setup Anthropic model step', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('configures Claude Code backend and validates local auth', async () => {
|
||||
const io = makeIo();
|
||||
const authProbe = vi.fn(async () => ({ ok: true as const }));
|
||||
|
||||
const result = await runKtxSetupAnthropicModelStep(
|
||||
{
|
||||
projectDir: tempDir,
|
||||
inputMode: 'disabled',
|
||||
llmBackend: 'claude-code',
|
||||
skipLlm: false,
|
||||
},
|
||||
io.io,
|
||||
{ claudeCodeAuthProbe: authProbe },
|
||||
);
|
||||
|
||||
expect(result.status).toBe('ready');
|
||||
const config = parseKtxProjectConfig(await readFile(join(tempDir, 'ktx.yaml'), 'utf-8'));
|
||||
expect(config.llm).toMatchObject({
|
||||
provider: { backend: 'claude-code' },
|
||||
models: { default: 'sonnet' },
|
||||
});
|
||||
expect(authProbe).toHaveBeenCalledWith(expect.objectContaining({ projectDir: tempDir, model: 'sonnet' }));
|
||||
});
|
||||
|
||||
it('returns from Anthropic credential Back to provider selection', async () => {
|
||||
const prompts = makePromptAdapter({ selectValues: ['anthropic', 'back', 'back'] });
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { execFile } from 'node:child_process';
|
||||
import { writeFile } from 'node:fs/promises';
|
||||
import { promisify } from 'node:util';
|
||||
import { resolveLocalKtxLlmConfig } from '@ktx/context';
|
||||
import { resolveLocalKtxLlmConfig, runClaudeCodeAuthProbe } from '@ktx/context';
|
||||
import { resolveKtxConfigReference } from '@ktx/context/core';
|
||||
import {
|
||||
type KtxProjectConfig,
|
||||
|
|
@ -53,7 +53,7 @@ export interface AnthropicModelChoice {
|
|||
recommended: boolean;
|
||||
}
|
||||
|
||||
export type KtxSetupLlmBackend = 'anthropic' | 'vertex';
|
||||
export type KtxSetupLlmBackend = 'anthropic' | 'vertex' | 'claude-code';
|
||||
|
||||
export interface KtxSetupModelPromptAdapter {
|
||||
select(options: { message: string; options: KtxSetupPromptOption[] }): Promise<string>;
|
||||
|
|
@ -68,6 +68,11 @@ export interface KtxSetupModelDeps {
|
|||
prompts?: KtxSetupModelPromptAdapter;
|
||||
listModels?: (apiKey: string) => Promise<AnthropicModelChoice[]>;
|
||||
healthCheck?: (config: KtxLlmConfig) => Promise<KtxLlmHealthCheckResult>;
|
||||
claudeCodeAuthProbe?: (input: {
|
||||
projectDir: string;
|
||||
model: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}) => Promise<{ ok: true } | { ok: false; message: string }>;
|
||||
readGcloudProject?: () => Promise<string | undefined>;
|
||||
listGcloudProjects?: () => Promise<GcloudProjectChoice[]>;
|
||||
spinner?: () => KtxCliSpinner;
|
||||
|
|
@ -252,7 +257,7 @@ export function isKtxSetupLlmConfigReady(config: KtxProjectLlmConfig): boolean {
|
|||
return typeof resolved.vertex?.location === 'string' && resolved.vertex.location.trim().length > 0;
|
||||
}
|
||||
|
||||
return resolved.backend === 'anthropic' || resolved.backend === 'gateway';
|
||||
return resolved.backend === 'anthropic' || resolved.backend === 'gateway' || resolved.backend === 'claude-code';
|
||||
}
|
||||
|
||||
function hasUsableConfiguredLlm(config: KtxProjectConfig): boolean {
|
||||
|
|
@ -263,9 +268,18 @@ function buildProjectLlmConfig(
|
|||
existing: KtxProjectLlmConfig,
|
||||
provider:
|
||||
| { backend: 'anthropic'; credentialRef: string }
|
||||
| { backend: 'vertex'; vertex: { project?: string; location: string } },
|
||||
| { backend: 'vertex'; vertex: { project?: string; location: string } }
|
||||
| { backend: 'claude-code' },
|
||||
model: string,
|
||||
): KtxProjectLlmConfig {
|
||||
if (provider.backend === 'claude-code') {
|
||||
return {
|
||||
provider: { backend: 'claude-code' },
|
||||
models: { ...existing.models, default: model },
|
||||
promptCaching: existing.promptCaching,
|
||||
};
|
||||
}
|
||||
|
||||
if (provider.backend === 'vertex') {
|
||||
return {
|
||||
provider: {
|
||||
|
|
@ -480,16 +494,21 @@ async function chooseBackend(
|
|||
}
|
||||
const choice = await prompts.select({
|
||||
message: 'Which LLM provider should KTX use?',
|
||||
options: [
|
||||
{ value: 'anthropic', label: 'Anthropic API' },
|
||||
{ value: 'vertex', label: 'Google Vertex AI for Anthropic Claude' },
|
||||
{ value: 'back', label: 'Back' },
|
||||
],
|
||||
options: [
|
||||
{ value: 'anthropic', label: 'Anthropic API' },
|
||||
{ value: 'vertex', label: 'Google Vertex AI for Anthropic Claude' },
|
||||
{ value: 'claude-code', label: 'Local Claude Code session' },
|
||||
{ value: 'back', label: 'Back' },
|
||||
],
|
||||
});
|
||||
if (choice === 'back') {
|
||||
return { status: 'back' };
|
||||
}
|
||||
return { status: 'ready', backend: choice === 'vertex' ? 'vertex' : 'anthropic', prompted: true };
|
||||
return {
|
||||
status: 'ready',
|
||||
backend: choice === 'vertex' || choice === 'claude-code' ? choice : 'anthropic',
|
||||
prompted: true,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveProvidedVertexRef(
|
||||
|
|
@ -807,7 +826,8 @@ async function persistLlmConfig(
|
|||
projectDir: string,
|
||||
provider:
|
||||
| { backend: 'anthropic'; credentialRef: string }
|
||||
| { backend: 'vertex'; vertex: { project?: string; location: string } },
|
||||
| { backend: 'vertex'; vertex: { project?: string; location: string } }
|
||||
| { backend: 'claude-code' },
|
||||
model: string,
|
||||
): Promise<void> {
|
||||
const project = await loadKtxProject({ projectDir });
|
||||
|
|
@ -918,6 +938,19 @@ export async function runKtxSetupAnthropicModelStep(
|
|||
continue;
|
||||
}
|
||||
|
||||
if (backendChoice.backend === 'claude-code') {
|
||||
const model = backendArgs.anthropicModel ?? 'sonnet';
|
||||
const probe = deps.claudeCodeAuthProbe ?? runClaudeCodeAuthProbe;
|
||||
const health = await probe({ projectDir: args.projectDir, model, env: deps.env ?? process.env });
|
||||
if (!health.ok) {
|
||||
io.stderr.write(`${health.message}\n`);
|
||||
return { status: 'failed', projectDir: args.projectDir };
|
||||
}
|
||||
await persistLlmConfig(args.projectDir, { backend: 'claude-code' }, model);
|
||||
io.stdout.write(`│ LLM ready: yes (${model})\n`);
|
||||
return { status: 'ready', projectDir: args.projectDir };
|
||||
}
|
||||
|
||||
const credential = await chooseCredentialRef(backendArgs, io, deps);
|
||||
if (credential.status === 'back' && backendChoice.prompted) {
|
||||
attemptArgs = buildInteractiveRetryArgs(args);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { basename } from 'node:path';
|
||||
import { runClaudeCodeAuthProbe } from '@ktx/context';
|
||||
import type {
|
||||
KtxConfigIssue,
|
||||
KtxLocalProject,
|
||||
|
|
@ -70,6 +71,12 @@ interface WarningItem {
|
|||
fix?: string;
|
||||
}
|
||||
|
||||
type ClaudeCodeAuthProbe = (input: {
|
||||
projectDir: string;
|
||||
model: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}) => Promise<{ ok: true } | { ok: false; message: string }>;
|
||||
|
||||
const PROJECT_READY_COMMANDS = KTX_NEXT_STEP_DIRECT_COMMANDS.map((step) => step.command);
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
|
|
@ -127,7 +134,15 @@ function envHint(value: unknown): string | undefined {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
function buildLlmStatus(config: KtxProjectLlmConfig, env: NodeJS.ProcessEnv): LlmStatus {
|
||||
async function buildLlmStatus(
|
||||
config: KtxProjectLlmConfig,
|
||||
options: {
|
||||
projectDir: string;
|
||||
env: NodeJS.ProcessEnv;
|
||||
claudeCodeAuthProbe?: ClaudeCodeAuthProbe;
|
||||
},
|
||||
): Promise<LlmStatus> {
|
||||
const env = options.env;
|
||||
const backend = config.provider.backend;
|
||||
const model = config.models?.default;
|
||||
if (backend === 'none') {
|
||||
|
|
@ -179,6 +194,26 @@ function buildLlmStatus(config: KtxProjectLlmConfig, env: NodeJS.ProcessEnv): Ll
|
|||
fix: hint ? `Set ${hint}` : 'Set the gateway api_key or rerun `ktx setup`',
|
||||
};
|
||||
}
|
||||
if (backend === 'claude-code') {
|
||||
const modelName = model ?? 'sonnet';
|
||||
const probe = options.claudeCodeAuthProbe ?? runClaudeCodeAuthProbe;
|
||||
const auth = await probe({ projectDir: options.projectDir, model: modelName, env });
|
||||
if (auth.ok) {
|
||||
return {
|
||||
backend,
|
||||
model: modelName,
|
||||
status: 'ok',
|
||||
detail: 'local Claude Code session authenticated',
|
||||
};
|
||||
}
|
||||
return {
|
||||
backend,
|
||||
model: modelName,
|
||||
status: 'fail',
|
||||
detail: auth.message,
|
||||
fix: 'Authenticate Claude Code locally with the Claude Code CLI, then rerun `ktx status`.',
|
||||
};
|
||||
}
|
||||
return { backend, model, status: 'warn', detail: 'unknown LLM backend' };
|
||||
}
|
||||
|
||||
|
|
@ -474,6 +509,13 @@ function buildPipelineStatus(config: KtxProjectConfig): PipelineStatus {
|
|||
};
|
||||
}
|
||||
|
||||
function ignoredClaudeCodePromptCachingFields(config: KtxProjectLlmConfig): string[] {
|
||||
if (config.provider.backend !== 'claude-code' || !config.promptCaching) {
|
||||
return [];
|
||||
}
|
||||
return Object.keys(config.promptCaching).map((key) => `llm.promptCaching.${key}`);
|
||||
}
|
||||
|
||||
function buildStorageStatus(config: KtxProjectConfig): StorageStatus {
|
||||
return {
|
||||
state: config.storage.state,
|
||||
|
|
@ -561,6 +603,14 @@ function buildWarnings(
|
|||
});
|
||||
}
|
||||
|
||||
const ignored = ignoredClaudeCodePromptCachingFields(config.llm);
|
||||
if (ignored.length > 0) {
|
||||
warnings.push({
|
||||
message: `claude-code ignores ${ignored.join(', ')} because the Claude Agent SDK does not expose KTX prompt-cache TTL, tool, or history markers.`,
|
||||
fix: 'Remove those promptCaching fields or use anthropic, vertex, or gateway when those cache knobs are required.',
|
||||
});
|
||||
}
|
||||
|
||||
return warnings;
|
||||
}
|
||||
|
||||
|
|
@ -622,6 +672,7 @@ function buildVerdict(
|
|||
export interface BuildProjectStatusOptions {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
postgresQueryHistoryProbe?: PostgresQueryHistoryProbe;
|
||||
claudeCodeAuthProbe?: ClaudeCodeAuthProbe;
|
||||
configIssues?: KtxConfigIssue[];
|
||||
}
|
||||
|
||||
|
|
@ -642,7 +693,11 @@ export async function buildProjectStatus(project: KtxLocalProject, options: Buil
|
|||
const config = project.config;
|
||||
|
||||
const configStatus = buildConfigStatus(options.configIssues);
|
||||
const llm = buildLlmStatus(config.llm, env);
|
||||
const llm = await buildLlmStatus(config.llm, {
|
||||
projectDir: project.projectDir,
|
||||
env,
|
||||
claudeCodeAuthProbe: options.claudeCodeAuthProbe,
|
||||
});
|
||||
const embeddings = buildEmbeddingsStatus(config.ingest.embeddings, env);
|
||||
const storage = buildStorageStatus(config);
|
||||
const connections = Object.entries(config.connections).map(([name, conn]) =>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue