diff --git a/packages/context/package.json b/packages/context/package.json index 28ec5190..104b4e47 100644 --- a/packages/context/package.json +++ b/packages/context/package.json @@ -129,6 +129,7 @@ "type-check": "tsc -p tsconfig.json --noEmit" }, "dependencies": { + "@anthropic-ai/claude-agent-sdk": "0.3.142", "@ktx/llm": "workspace:*", "@looker/sdk": "^26.8.0", "@looker/sdk-node": "^26.8.0", diff --git a/packages/context/src/project/config.test.ts b/packages/context/src/project/config.test.ts index 164282eb..3967b363 100644 --- a/packages/context/src/project/config.test.ts +++ b/packages/context/src/project/config.test.ts @@ -180,6 +180,31 @@ llm: }); }); + it('parses Claude Code as a first-class LLM backend', () => { + const config = parseKtxProjectConfig(` +llm: + provider: + backend: claude-code + models: + default: sonnet + triage: haiku + candidateExtraction: sonnet + curator: sonnet + reconcile: sonnet + repair: opus +`); + + expect(config.llm.provider.backend).toBe('claude-code'); + expect(config.llm.models).toEqual({ + default: 'sonnet', + triage: 'haiku', + candidateExtraction: 'sonnet', + curator: 'sonnet', + reconcile: 'sonnet', + repair: 'opus', + }); + }); + it('parses gateway LLM, OpenAI scan embeddings, and sentence-transformers ingest embeddings', () => { const config = parseKtxProjectConfig(` llm: @@ -497,7 +522,7 @@ describe('generateKtxProjectConfigJsonSchema', () => { const llm = (schema.properties as Record }>).llm; const provider = llm?.properties?.provider as { properties?: Record }; const backend = provider?.properties?.backend as { enum?: readonly string[] }; - expect(backend?.enum).toEqual(['none', 'anthropic', 'vertex', 'gateway']); + expect(backend?.enum).toEqual(['none', 'anthropic', 'vertex', 'gateway', 'claude-code']); const storage = (schema.properties as Record }>).storage; const state = storage?.properties?.state as { enum?: readonly string[] }; diff --git a/packages/context/src/project/config.ts b/packages/context/src/project/config.ts index 178721c4..912c31de 100644 --- a/packages/context/src/project/config.ts +++ b/packages/context/src/project/config.ts @@ -3,7 +3,7 @@ import YAML from 'yaml'; import * as z from 'zod'; import { connectionConfigSchema } from './driver-schemas.js'; -const KTX_LLM_BACKENDS = ['none', 'anthropic', 'vertex', 'gateway'] as const; +const KTX_LLM_BACKENDS = ['none', 'anthropic', 'vertex', 'gateway', 'claude-code'] as const; const KTX_EMBEDDING_BACKENDS = ['none', 'deterministic', 'openai', 'sentence-transformers'] as const; const KTX_PROMPT_CACHE_TTLS = ['5m', '1h'] as const; const KTX_ENRICHMENT_MODES = ['none', 'deterministic', 'llm'] as const; @@ -46,7 +46,9 @@ const llmProviderSchema = z backend: z .enum(KTX_LLM_BACKENDS) .default('none') - .describe('LLM provider backend. "none" disables LLM features; "anthropic" / "vertex" / "gateway" require the matching nested credentials block.'), + .describe( + 'LLM provider backend. "none" disables LLM features; "anthropic" / "vertex" / "gateway" require the matching nested credentials block; "claude-code" uses the local Claude Code session.', + ), vertex: vertexProviderSchema.optional().describe('Vertex AI credentials, used when backend is "vertex".'), anthropic: apiCredentialsSchema.optional().describe('Anthropic API credentials, used when backend is "anthropic".'), gateway: apiCredentialsSchema.optional().describe('AI Gateway credentials, used when backend is "gateway".'), diff --git a/packages/llm/src/model-health.test.ts b/packages/llm/src/model-health.test.ts index 8752b09e..8cf7a7ee 100644 --- a/packages/llm/src/model-health.test.ts +++ b/packages/llm/src/model-health.test.ts @@ -61,4 +61,17 @@ describe('KTX LLM health check', () => { message: '401 invalid x-api-key [redacted]', }); }); + + it('reports claude-code as unsupported by the AI SDK health check', async () => { + const result = await runKtxLlmHealthCheck({ + backend: 'claude-code', + modelSlots: { default: 'sonnet' }, + promptCaching: { enabled: false }, + }); + + expect(result).toEqual({ + ok: false, + message: expect.stringContaining('claude-code is not an AI SDK LanguageModel backend'), + }); + }); }); diff --git a/packages/llm/src/model-provider.test.ts b/packages/llm/src/model-provider.test.ts index 961b0fdb..1a61d0a1 100644 --- a/packages/llm/src/model-provider.test.ts +++ b/packages/llm/src/model-provider.test.ts @@ -302,4 +302,14 @@ describe('createKtxLlmProvider', () => { expect(provider.promptCachingConfig().enabled).toBe(false); expect(provider.cacheMarker('1h', 'claude-sonnet-4-6')).toBeUndefined(); }); + + it('throws instead of falling through when an unsupported LLM backend is passed to the AI SDK provider factory', () => { + expect(() => + createKtxLlmProvider({ + backend: 'claude-code', + modelSlots: { default: 'sonnet' }, + promptCaching: { enabled: false }, + }), + ).toThrow('claude-code is not an AI SDK LanguageModel backend'); + }); }); diff --git a/packages/llm/src/model-provider.ts b/packages/llm/src/model-provider.ts index 392a16e1..86b9270b 100644 --- a/packages/llm/src/model-provider.ts +++ b/packages/llm/src/model-provider.ts @@ -175,14 +175,18 @@ class DefaultKtxLlmProvider implements KtxLlmProvider { return (modelId) => vertex(modelId); } - const gateway = (deps.createGateway ?? createGateway)({ - ...(config.gateway?.apiKey ? { apiKey: config.gateway.apiKey } : {}), - ...(config.gateway?.baseURL ? { baseURL: config.gateway.baseURL } : {}), - headers: { - 'anthropic-beta': ANTHROPIC_BETA_HEADER, - }, - }); - return (modelId) => gateway(modelId); + if (config.backend === 'gateway') { + const gateway = (deps.createGateway ?? createGateway)({ + ...(config.gateway?.apiKey ? { apiKey: config.gateway.apiKey } : {}), + ...(config.gateway?.baseURL ? { baseURL: config.gateway.baseURL } : {}), + headers: { + 'anthropic-beta': ANTHROPIC_BETA_HEADER, + }, + }); + return (modelId) => gateway(modelId); + } + + throw new Error(`${config.backend} is not an AI SDK LanguageModel backend; use KtxLlmRuntimePort`); } } diff --git a/packages/llm/src/types.ts b/packages/llm/src/types.ts index a477b2f2..b91aec25 100644 --- a/packages/llm/src/types.ts +++ b/packages/llm/src/types.ts @@ -3,7 +3,7 @@ import type { LanguageModel, TelemetrySettings, ToolCallRepairFunction, ToolSet export const KTX_MODEL_ROLES = ['default', 'triage', 'candidateExtraction', 'curator', 'reconcile', 'repair'] as const; export type KtxModelRole = (typeof KTX_MODEL_ROLES)[number]; -export type KtxLlmBackend = 'anthropic' | 'vertex' | 'gateway'; +export type KtxLlmBackend = 'anthropic' | 'vertex' | 'gateway' | 'claude-code'; export type KtxPromptCacheTtl = '5m' | '1h'; export type KtxJsonValue = diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 22e0c035..cd331dd4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -312,6 +312,9 @@ importers: packages/context: dependencies: + '@anthropic-ai/claude-agent-sdk': + specifier: 0.3.142 + version: 0.3.142(zod@4.4.3) '@ktx/llm': specifier: workspace:* version: link:../llm @@ -475,6 +478,65 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} + '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.142': + resolution: {integrity: sha512-yBHOiRqJ8JcD9OAMGJALbypaD3u3K8hyUmcnZ+91AHJtymzWxuMkVi4IY1qp8L5jzkKeTnvYfCspzkbiHLuYWg==} + cpu: [arm64] + os: [darwin] + + '@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.142': + resolution: {integrity: sha512-/a/bVMjvAl3gNzWiPIgynYktTYckTcp4YAacV/2F4Jd8XeCV0+DMQW7OFeR+3fnPcBg/8kcOAVYfLZXDExqO1w==} + cpu: [x64] + os: [darwin] + + '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.142': + resolution: {integrity: sha512-KZuwSupNJovnMJ7MZxjp1Qq0yu7rAmbzO4Zlmr3jtKDU95t2kgs3c6j4evzQDCgTQMlwH8QTSV4mItDGxlYEbg==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.142': + resolution: {integrity: sha512-QKG553PSbIcQ5KLvnl2ekfy5lTyU3dW/X5fDQlRLv4YHNHnqf2o7scJ6eUdfaVTQdIZ+Pa7SNN3bsvVs4bNjQw==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.142': + resolution: {integrity: sha512-QkDwLMsdYO7n/i1zPCt5YZIet5u+Eo07UpF9UX5yD7bnwRZKDe22L6LVVwiLLjeTO0fTz+uNY7w9/XOYQMlxUQ==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@anthropic-ai/claude-agent-sdk-linux-x64@0.3.142': + resolution: {integrity: sha512-o1QZmCNRL5BFTc14KEvT23Fxm1jNv0aa0e9T0OZUjua0oW8DRpri3HKvDEM36qEGWUOANBG7h6Ca/KNqxaTnYg==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.142': + resolution: {integrity: sha512-x8lbY1m7E/BiFF0Gu/Mx9lkD/zW3vBr3viw0GYNuqY9GYHfLOX9+l9H8C+INeGzB4+ibG8+xD2pnRhWdQxuvUg==} + cpu: [arm64] + os: [win32] + + '@anthropic-ai/claude-agent-sdk-win32-x64@0.3.142': + resolution: {integrity: sha512-NpNxdiCEUNjjwvBltpDnkgdjVQ+nRsALpfM1Pe4GhnYiOkTk/TvjMZUuA2qGh0F8KyF0FbqzUsi0uXIgojJT5w==} + cpu: [x64] + os: [win32] + + '@anthropic-ai/claude-agent-sdk@0.3.142': + resolution: {integrity: sha512-k1xBon6ov0PT/vZNf+Z+SuAqmylGJU/+a+h/k04MW5cBbzOIwiVcGFRTGJ/qbY5pcboJbLtts/yBwSu9AvSipg==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: ^4.0.0 + + '@anthropic-ai/sdk@0.93.0': + resolution: {integrity: sha512-q9vaSZQVFx6B/gPxetGYfLXSJD5v0sOmh0OpZDq7yCrTSA+Rscvrtyol7JJTW40wEpQB4U1B4JXzxQitbQ3CAA==} + hasBin: true + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + peerDependenciesMeta: + zod: + optional: true + '@aws-crypto/crc32@5.2.0': resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} engines: {node: '>=16.0.0'} @@ -799,6 +861,10 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + '@babel/runtime@7.29.2': + resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} + engines: {node: '>=6.9.0'} + '@babel/types@7.29.0': resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} @@ -3656,6 +3722,10 @@ packages: json-bigint@1.0.0: resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} + json-schema-to-ts@3.1.1: + resolution: {integrity: sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==} + engines: {node: '>=16'} + json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} @@ -4752,6 +4822,9 @@ packages: trough@2.2.0: resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + ts-algebra@2.0.0: + resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -5080,6 +5153,54 @@ snapshots: '@alloc/quick-lru@5.2.0': {} + '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.142': + optional: true + + '@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.142': + optional: true + + '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.142': + optional: true + + '@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.142': + optional: true + + '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.142': + optional: true + + '@anthropic-ai/claude-agent-sdk-linux-x64@0.3.142': + optional: true + + '@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.142': + optional: true + + '@anthropic-ai/claude-agent-sdk-win32-x64@0.3.142': + optional: true + + '@anthropic-ai/claude-agent-sdk@0.3.142(zod@4.4.3)': + dependencies: + '@anthropic-ai/sdk': 0.93.0(zod@4.4.3) + '@modelcontextprotocol/sdk': 1.29.0(zod@4.4.3) + zod: 4.4.3 + optionalDependencies: + '@anthropic-ai/claude-agent-sdk-darwin-arm64': 0.3.142 + '@anthropic-ai/claude-agent-sdk-darwin-x64': 0.3.142 + '@anthropic-ai/claude-agent-sdk-linux-arm64': 0.3.142 + '@anthropic-ai/claude-agent-sdk-linux-arm64-musl': 0.3.142 + '@anthropic-ai/claude-agent-sdk-linux-x64': 0.3.142 + '@anthropic-ai/claude-agent-sdk-linux-x64-musl': 0.3.142 + '@anthropic-ai/claude-agent-sdk-win32-arm64': 0.3.142 + '@anthropic-ai/claude-agent-sdk-win32-x64': 0.3.142 + transitivePeerDependencies: + - '@cfworker/json-schema' + - supports-color + + '@anthropic-ai/sdk@0.93.0(zod@4.4.3)': + dependencies: + json-schema-to-ts: 3.1.1 + optionalDependencies: + zod: 4.4.3 + '@aws-crypto/crc32@5.2.0': dependencies: '@aws-crypto/util': 5.2.0 @@ -6002,6 +6123,8 @@ snapshots: dependencies: '@babel/types': 7.29.0 + '@babel/runtime@7.29.2': {} + '@babel/types@7.29.0': dependencies: '@babel/helper-string-parser': 7.27.1 @@ -6443,6 +6566,7 @@ snapshots: '@ktx/context@file:packages/context(js-yaml@4.1.1)': dependencies: + '@anthropic-ai/claude-agent-sdk': 0.3.142(zod@4.4.3) '@ktx/llm': file:packages/llm(zod@4.4.3) '@looker/sdk': 26.8.0 '@looker/sdk-node': 26.8.0 @@ -6468,6 +6592,7 @@ snapshots: '@ktx/context@file:packages/context(js-yaml@4.1.1)(ws@8.20.0)': dependencies: + '@anthropic-ai/claude-agent-sdk': 0.3.142(zod@4.4.3) '@ktx/llm': file:packages/llm(ws@8.20.0)(zod@4.4.3) '@looker/sdk': 26.8.0 '@looker/sdk-node': 26.8.0 @@ -8884,6 +9009,11 @@ snapshots: dependencies: bignumber.js: 9.3.1 + json-schema-to-ts@3.1.1: + dependencies: + '@babel/runtime': 7.29.2 + ts-algebra: 2.0.0 + json-schema-traverse@1.0.0: {} json-schema-typed@8.0.2: {} @@ -10439,6 +10569,8 @@ snapshots: trough@2.2.0: {} + ts-algebra@2.0.0: {} + tslib@2.8.1: {} tunnel-agent@0.6.0: