import { describe, expect, it, vi } from 'vitest'; import { buildDefaultKtxProjectConfig, type KtxProjectEmbeddingConfig, type KtxProjectLlmConfig, } from '../../../src/context/project/config.js'; import { createLocalKtxEmbeddingProviderFromConfig, createLocalKtxLlmProviderFromConfig, resolveLocalKtxEmbeddingConfig, resolveLocalKtxLlmConfig, } from '../../../src/context/llm/local-config.js'; describe('local KTX LLM config', () => { it('resolves env and file references into a KtxLlmConfig', () => { const config: KtxProjectLlmConfig = { provider: { backend: 'gateway', gateway: { api_key: 'env:AI_GATEWAY_API_KEY', base_url: 'https://gateway.example/v1' }, // pragma: allowlist secret }, models: { default: 'env:KTX_MODEL', triage: 'anthropic/claude-haiku-4-5' }, promptCaching: { enabled: false }, }; expect( resolveLocalKtxLlmConfig(config, { AI_GATEWAY_API_KEY: 'gateway-key', // pragma: allowlist secret KTX_MODEL: 'anthropic/claude-sonnet-4-6', }), ).toEqual({ backend: 'gateway', gateway: { apiKey: 'gateway-key', baseURL: 'https://gateway.example/v1' }, // pragma: allowlist secret modelSlots: { default: 'anthropic/claude-sonnet-4-6', triage: 'anthropic/claude-haiku-4-5' }, promptCaching: { enabled: false }, }); }); it('resolves Vertex AI env references into a KtxLlmConfig', () => { const config: KtxProjectLlmConfig = { provider: { backend: 'vertex', vertex: { project: 'env:GOOGLE_VERTEX_PROJECT', location: 'env:GOOGLE_VERTEX_LOCATION' }, }, models: { default: 'env:KTX_MODEL' }, promptCaching: { enabled: true, vertexFallbackTo5m: true }, }; expect( resolveLocalKtxLlmConfig(config, { GOOGLE_VERTEX_PROJECT: 'local-gcp-project', GOOGLE_VERTEX_LOCATION: 'us-east5', KTX_MODEL: 'claude-sonnet-4-6', }), ).toEqual({ backend: 'vertex', vertex: { project: 'local-gcp-project', location: 'us-east5' }, modelSlots: { default: 'claude-sonnet-4-6' }, promptCaching: { enabled: true, vertexFallbackTo5m: true }, }); }); it('ignores inactive Vertex AI references for non-Vertex backends', () => { const config: KtxProjectLlmConfig = { provider: { backend: 'anthropic', anthropic: { api_key: 'env:ANTHROPIC_API_KEY' }, // pragma: allowlist secret vertex: { location: 'env:MISSING_VERTEX_LOCATION' }, }, models: { default: 'claude-sonnet-4-6' }, }; expect( resolveLocalKtxLlmConfig(config, { ANTHROPIC_API_KEY: 'sk-ant-test', // pragma: allowlist secret }), ).toEqual({ backend: 'anthropic', anthropic: { apiKey: 'sk-ant-test' }, // pragma: allowlist secret modelSlots: { default: 'claude-sonnet-4-6' }, promptCaching: undefined, }); }); it('returns null when the local LLM backend is disabled', () => { expect( createLocalKtxLlmProviderFromConfig({ provider: { backend: 'none' }, models: {}, }), ).toBeNull(); }); it('constructs providers through LLM modules', () => { const createKtxLlmProvider = vi.fn(() => ({ getModel: vi.fn() }) as never); const result = createLocalKtxLlmProviderFromConfig( { provider: { backend: 'anthropic', anthropic: { api_key: 'env:ANTHROPIC_API_KEY' }, // pragma: allowlist secret }, models: { default: 'claude-sonnet-4-6' }, }, { env: { ANTHROPIC_API_KEY: 'sk-ant-test' }, createKtxLlmProvider }, // pragma: allowlist secret ); expect(result).not.toBeNull(); expect(createKtxLlmProvider).toHaveBeenCalledWith({ backend: 'anthropic', anthropic: { apiKey: 'sk-ant-test' }, // pragma: allowlist secret modelSlots: { default: 'claude-sonnet-4-6' }, promptCaching: undefined, }); }); it('inherits enabled prompt caching from LLM modules when local config omits promptCaching', () => { const provider = createLocalKtxLlmProviderFromConfig({ provider: { backend: 'gateway', gateway: { base_url: 'https://gateway.example/v1' }, }, models: { default: 'anthropic/claude-sonnet-4-6' }, }); expect(provider?.promptCachingConfig()).toMatchObject({ enabled: true, systemTtl: '1h', toolsTtl: '1h', historyTtl: '5m', vertexFallbackTo5m: false, }); }); }); describe('local KTX embedding config', () => { it('resolves sentence-transformers config', () => { const config: KtxProjectEmbeddingConfig = { backend: 'sentence-transformers', model: 'all-MiniLM-L6-v2', dimensions: 384, sentenceTransformers: { base_url: 'http://localhost:18081', pathPrefix: '' }, batchSize: 16, }; expect(resolveLocalKtxEmbeddingConfig(config, {})).toEqual({ backend: 'sentence-transformers', model: 'all-MiniLM-L6-v2', dimensions: 384, sentenceTransformers: { baseURL: 'http://localhost:18081', pathPrefix: '' }, batchSize: 16, }); }); it('returns null when sentence-transformers has no base_url (managed daemon delegation)', () => { const config: KtxProjectEmbeddingConfig = { backend: 'sentence-transformers', model: 'all-MiniLM-L6-v2', dimensions: 384, sentenceTransformers: { base_url: '', pathPrefix: '', }, }; expect(resolveLocalKtxEmbeddingConfig(config, {})).toBeNull(); }); it('returns null when backend is openai but no apiKey is resolvable from env', () => { const config: KtxProjectEmbeddingConfig = { backend: 'openai', model: 'text-embedding-3-small', dimensions: 1536, openai: { api_key: 'env:OPENAI_API_KEY' }, // pragma: allowlist secret }; expect(resolveLocalKtxEmbeddingConfig(config, {})).toBeNull(); }); it('resolves openai embedding config from env', () => { const config: KtxProjectEmbeddingConfig = { backend: 'openai', model: 'text-embedding-3-small', dimensions: 1536, openai: { api_key: 'env:OPENAI_API_KEY' }, // pragma: allowlist secret }; expect( resolveLocalKtxEmbeddingConfig(config, { OPENAI_API_KEY: 'sk-test' }), // pragma: allowlist secret ).toEqual({ backend: 'openai', model: 'text-embedding-3-small', dimensions: 1536, openai: { apiKey: 'sk-test' }, // pragma: allowlist secret batchSize: undefined, }); }); it('returns null for the default disabled project embedding config', () => { const createKtxEmbeddingProvider = vi.fn(() => ({}) as never); const provider = createLocalKtxEmbeddingProviderFromConfig( buildDefaultKtxProjectConfig().ingest.embeddings, { createKtxEmbeddingProvider }, ); expect(provider).toBeNull(); expect(createKtxEmbeddingProvider).not.toHaveBeenCalled(); }); it('returns null when embeddings are disabled', () => { expect(createLocalKtxEmbeddingProviderFromConfig({ backend: 'none', dimensions: 8 })).toBeNull(); }); });