feat: add claude-code llm backend with runtime port (#115)

* docs: revise claude-code ingest backend spec

* docs: keep claude-code spec focused on ingest

* docs: expand claude-code spec to full llm parity

* Refine claude-code backend spec after adversarial review iteration 1

* Refine claude-code backend spec after adversarial review iteration 2

* Refine claude-code backend spec after adversarial review iteration 3

* feat: recognize claude-code llm backend

* feat: add ktx llm runtime port

* feat: add claude-code llm runtime

* feat: route non-agent llm calls through runtime

* feat: run ingest agents through llm runtime

* feat: support claude-code setup and status

* test: verify claude-code backend runtime

* docs: add claude-code backend v1 runtime plan

* fix: close claude-code runtime isolation checks

* fix: warn on claude-code prompt caching during setup

* chore: verify claude-code v1 closure

* docs: add claude-code backend v1 isolation closure plan

* fix: update claude-code ingest setup guidance

* docs: add claude-code backend v1 ingest guidance closure plan

* docs: align claude-code isolation spec with sdk metadata

* test: cover claude-code host discovery metadata

* fix: tolerate claude-code host discovery metadata

* docs: clarify claude-code host discovery metadata

* docs: add claude-code auth-probe isolation fix plan

* chore: prepare kaelio ktx rc1 release

* chore: add semantic release workflow

* fix: unblock ci checks

* chore(release): 0.1.0-rc.1

* feat: add Claude Code model selection to setup

* fix: keep git maintenance attached in local repos
This commit is contained in:
Andrey Avtomonov 2026-05-16 12:06:34 +02:00 committed by GitHub
parent e6d578c03f
commit b565e44a22
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
109 changed files with 10218 additions and 1093 deletions

View file

@ -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,100 @@ 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('prompts for the Claude Code model during interactive setup', async () => {
const io = makeIo();
const prompts = makePromptAdapter({ selectValues: ['claude-code', 'opus'] });
const authProbe = vi.fn(async () => ({ ok: true as const }));
const result = await runKtxSetupAnthropicModelStep(
{ projectDir: tempDir, inputMode: 'auto', skipLlm: false },
io.io,
{ prompts, claudeCodeAuthProbe: authProbe },
);
expect(result.status).toBe('ready');
expect(prompts.select).toHaveBeenCalledWith(
expect.objectContaining({
message: expect.stringContaining('Which Claude Code model should KTX use?'),
options: [
{ value: 'sonnet', label: 'Claude Sonnet', hint: 'recommended' },
{ value: 'opus', label: 'Claude Opus' },
{ value: 'haiku', label: 'Claude Haiku' },
{ value: 'manual', label: 'Enter a Claude Code model ID manually' },
{ value: 'back', label: 'Back' },
],
}),
);
const config = parseKtxProjectConfig(await readFile(join(tempDir, 'ktx.yaml'), 'utf-8'));
expect(config.llm).toMatchObject({
provider: { backend: 'claude-code' },
models: { default: 'opus' },
});
expect(authProbe).toHaveBeenCalledWith(expect.objectContaining({ projectDir: tempDir, model: 'opus' }));
});
it('warns during Claude Code setup when existing prompt-caching fields will be ignored', async () => {
await writeFile(
join(tempDir, 'ktx.yaml'),
[
'llm:',
' provider:',
' backend: anthropic',
' models:',
' default: claude-sonnet-4-6',
' promptCaching:',
' enabled: true',
' systemTtl: 1h',
' toolsTtl: 1h',
' historyTtl: 5m',
'',
].join('\n'),
'utf-8',
);
const io = makeIo();
const result = await runKtxSetupAnthropicModelStep(
{
projectDir: tempDir,
inputMode: 'disabled',
llmBackend: 'claude-code',
skipLlm: false,
},
io.io,
{
claudeCodeAuthProbe: async () => ({ ok: true as const }),
},
);
expect(result.status).toBe('ready');
expect(io.stderr()).toContain('claude-code ignores llm.promptCaching.systemTtl');
expect(io.stderr()).toContain('Claude Agent SDK does not expose KTX prompt-cache TTL, tool, or history markers');
});
it('returns from Anthropic credential Back to provider selection', async () => {
const prompts = makePromptAdapter({ selectValues: ['anthropic', 'back', 'back'] });
@ -649,7 +748,7 @@ describe('setup Anthropic model step', () => {
expect(io.stderr()).not.toContain('--skip-llm');
});
it('does not recommend skipping when non-interactive setup is missing an Anthropic model', async () => {
it('does not recommend skipping when non-interactive setup is missing an LLM model', async () => {
const io = makeIo();
const healthCheck = vi.fn(async () => ({ ok: true as const }));
@ -666,7 +765,7 @@ describe('setup Anthropic model step', () => {
expect(result.status).toBe('missing-input');
expect(healthCheck).not.toHaveBeenCalled();
expect(io.stderr()).toContain('Missing Anthropic model: pass --anthropic-model.');
expect(io.stderr()).toContain('Missing LLM model: pass --llm-model.');
expect(io.stderr()).not.toContain('--skip-llm');
});