fix(cli): resolve managed-embeddings daemon URL at project boundary

A clean `ktx setup` was failing verification because the managed
local-embeddings daemon URL was passed library-side through
`process.env[KTX_MANAGED_SENTENCE_TRANSFORMERS_BASE_URL]`, and the setup
flow never wrote that variable. With no resolved URL the embedding
provider was null, the deep scan emitted
`scan_enrichment_backend_not_configured`, descriptions + embeddings
stayed `skipped`, and the agent-readiness check exited 1.

Replace the env-var indirection with CLI-side substitution at the
project-load boundary. New `loadKtxCliProject` wraps `loadKtxProject`,
ensures the managed daemon when `managed:local-embeddings` is present in
`config.ingest.embeddings` or `config.scan.enrichment.embeddings`, and
substitutes the resolved baseUrl into the in-memory config. Runtime
entry points (scan, ingest, public-ingest, admin-reindex) use the new
loader; setup-time persistence paths keep raw `loadKtxProject` so the
on-disk `ktx.yaml` keeps the portable sentinel.

Cleanup follows from the new design: drop
`MANAGED_SENTENCE_TRANSFORMERS_BASE_URL_ENV`, remove the env-var lookup
branch in `resolveSentenceTransformersBaseUrl`, drop the `env` field
from `ManagedLocalEmbeddingsDaemon`, and collapse the manual
daemon-ensure dance in `admin-reindex.ts`.
This commit is contained in:
Andrey Avtomonov 2026-05-20 14:38:23 +02:00
parent da6d05ed55
commit 38376eece4
12 changed files with 318 additions and 95 deletions

View file

@ -1,18 +1,17 @@
import {
createLocalKtxEmbeddingProviderFromConfig,
KtxIngestEmbeddingPortAdapter,
MANAGED_SENTENCE_TRANSFORMERS_BASE_URL,
type KtxEmbeddingPort,
} from '@ktx/context';
import { reindexLocalIndexes, type ReindexScopeResult, type ReindexSummary } from '@ktx/context/index-sync';
import { loadKtxProject, type KtxLocalProject } from '@ktx/context/project';
import { type KtxLocalProject } from '@ktx/context/project';
import { Option, type Command } from '@commander-js/extra-typings';
import { cancel, intro, log, note, outro } from '@clack/prompts';
import type { KtxCliCommandContext } from './cli-program.js';
import { loadKtxCliProject } from './cli-project.js';
import type { KtxCliIo } from './cli-runtime.js';
import { resolveOutputMode } from './io/mode.js';
import { green, red, SYMBOLS } from './io/symbols.js';
import { ensureManagedLocalEmbeddingsDaemon } from './managed-local-embeddings.js';
export interface KtxAdminReindexArgs {
projectDir: string;
@ -49,30 +48,11 @@ export function registerAdminReindexCommand(admin: Command, context: KtxCliComma
});
}
async function resolveReindexEmbeddingService(
project: KtxLocalProject,
args: KtxAdminReindexArgs,
io: KtxCliIo,
): Promise<KtxEmbeddingPort | null> {
function resolveReindexEmbeddingService(project: KtxLocalProject): KtxEmbeddingPort | null {
const config = project.config.ingest.embeddings;
if (config.backend === 'none') {
return null;
}
if (
config.backend === 'sentence-transformers' &&
config.sentenceTransformers?.base_url === MANAGED_SENTENCE_TRANSFORMERS_BASE_URL
) {
const daemon = await ensureManagedLocalEmbeddingsDaemon({
cliVersion: args.cliVersion,
projectDir: project.projectDir,
installPolicy: 'never',
io,
});
const provider = createLocalKtxEmbeddingProviderFromConfig(config, { env: { ...process.env, ...daemon.env } });
return provider ? new KtxIngestEmbeddingPortAdapter(provider) : null;
}
const provider = createLocalKtxEmbeddingProviderFromConfig(config);
return provider ? new KtxIngestEmbeddingPortAdapter(provider) : null;
}
@ -186,8 +166,13 @@ function renderReindexPretty(summary: ReindexSummary, io: KtxCliIo): void {
async function runKtxAdminReindex(args: KtxAdminReindexArgs, io: KtxCliIo = process): Promise<number> {
try {
const project = await loadKtxProject({ projectDir: args.projectDir });
const embeddingService = await resolveReindexEmbeddingService(project, args, io);
const project = await loadKtxCliProject({
projectDir: args.projectDir,
cliVersion: args.cliVersion,
installPolicy: 'never',
io,
});
const embeddingService = resolveReindexEmbeddingService(project);
const summary = await reindexLocalIndexes(project, { force: args.force, embeddingService });
const mode = resolveOutputMode({ explicit: args.output, json: args.json, io });

View file

@ -0,0 +1,182 @@
import { describe, expect, it, vi } from 'vitest';
import { MANAGED_SENTENCE_TRANSFORMERS_BASE_URL } from '@ktx/context';
import { buildDefaultKtxProjectConfig, type KtxLocalProject, type KtxProjectConfig } from '@ktx/context/project';
import {
loadKtxCliProject,
projectNeedsManagedLocalEmbeddings,
substituteManagedLocalEmbeddingsUrl,
} from './cli-project.js';
import type { ManagedLocalEmbeddingsDaemon } from './managed-local-embeddings.js';
const RESOLVED_BASE_URL = 'http://127.0.0.1:51234';
function makeIo() {
let stderr = '';
return {
io: {
stdout: { write: (_chunk: string) => {} },
stderr: {
write: (chunk: string) => {
stderr += chunk;
},
},
},
stderr: () => stderr,
};
}
function projectWithConfig(config: KtxProjectConfig): KtxLocalProject {
return {
projectDir: '/work/proj',
configPath: '/work/proj/ktx.yaml',
config,
coreConfig: {} as KtxLocalProject['coreConfig'],
git: {} as KtxLocalProject['git'],
fileStore: {} as KtxLocalProject['fileStore'],
};
}
function withManagedIngestEmbedding(config: KtxProjectConfig): KtxProjectConfig {
return {
...config,
ingest: {
...config.ingest,
embeddings: {
backend: 'sentence-transformers',
model: 'all-MiniLM-L6-v2',
dimensions: 384,
sentenceTransformers: { base_url: MANAGED_SENTENCE_TRANSFORMERS_BASE_URL, pathPrefix: '' },
},
},
};
}
function withManagedScanEnrichmentEmbedding(config: KtxProjectConfig): KtxProjectConfig {
return {
...config,
scan: {
...config.scan,
enrichment: {
...config.scan.enrichment,
embeddings: {
backend: 'sentence-transformers',
model: 'all-MiniLM-L6-v2',
dimensions: 384,
sentenceTransformers: { base_url: MANAGED_SENTENCE_TRANSFORMERS_BASE_URL, pathPrefix: '' },
},
},
},
};
}
const fakeDaemon: ManagedLocalEmbeddingsDaemon = {
baseUrl: RESOLVED_BASE_URL,
stdoutLog: '/work/proj/.ktx/runtime/daemon.stdout.log',
stderrLog: '/work/proj/.ktx/runtime/daemon.stderr.log',
};
describe('projectNeedsManagedLocalEmbeddings', () => {
it('returns false when neither ingest nor scan embeddings reference the managed sentinel', () => {
expect(projectNeedsManagedLocalEmbeddings(buildDefaultKtxProjectConfig())).toBe(false);
});
it('returns true when ingest.embeddings uses the managed sentinel', () => {
expect(projectNeedsManagedLocalEmbeddings(withManagedIngestEmbedding(buildDefaultKtxProjectConfig()))).toBe(true);
});
it('returns true when scan.enrichment.embeddings uses the managed sentinel', () => {
expect(
projectNeedsManagedLocalEmbeddings(withManagedScanEnrichmentEmbedding(buildDefaultKtxProjectConfig())),
).toBe(true);
});
});
describe('substituteManagedLocalEmbeddingsUrl', () => {
it('rewrites the managed sentinel in both ingest.embeddings and scan.enrichment.embeddings', () => {
const config = withManagedScanEnrichmentEmbedding(withManagedIngestEmbedding(buildDefaultKtxProjectConfig()));
const resolved = substituteManagedLocalEmbeddingsUrl(config, RESOLVED_BASE_URL);
expect(resolved.ingest.embeddings.sentenceTransformers?.base_url).toBe(RESOLVED_BASE_URL);
expect(resolved.scan.enrichment.embeddings?.sentenceTransformers?.base_url).toBe(RESOLVED_BASE_URL);
});
it('returns the input unchanged when no sentinel is present', () => {
const config = buildDefaultKtxProjectConfig();
const resolved = substituteManagedLocalEmbeddingsUrl(config, RESOLVED_BASE_URL);
expect(resolved.ingest.embeddings).toEqual(config.ingest.embeddings);
expect(resolved.scan.enrichment.embeddings).toEqual(config.scan.enrichment.embeddings);
});
it('does not touch non-sentinel sentence-transformers URLs', () => {
const config: KtxProjectConfig = {
...buildDefaultKtxProjectConfig(),
ingest: {
...buildDefaultKtxProjectConfig().ingest,
embeddings: {
backend: 'sentence-transformers',
model: 'all-MiniLM-L6-v2',
dimensions: 384,
sentenceTransformers: { base_url: 'http://localhost:9999', pathPrefix: '' },
},
},
};
const resolved = substituteManagedLocalEmbeddingsUrl(config, RESOLVED_BASE_URL);
expect(resolved.ingest.embeddings.sentenceTransformers?.base_url).toBe('http://localhost:9999');
});
});
describe('loadKtxCliProject', () => {
it('returns the project unchanged and does not start the daemon when no sentinel is present', async () => {
const io = makeIo();
const project = projectWithConfig(buildDefaultKtxProjectConfig());
const loadProject = vi.fn(async () => project);
const ensureLocalEmbeddings = vi.fn(async () => fakeDaemon);
const result = await loadKtxCliProject(
{ projectDir: '/work/proj', cliVersion: '0.2.0', installPolicy: 'never', io: io.io },
{ loadProject, ensureLocalEmbeddings },
);
expect(result).toBe(project);
expect(ensureLocalEmbeddings).not.toHaveBeenCalled();
});
it('starts the daemon and substitutes the resolved URL when ingest.embeddings uses the sentinel', async () => {
const io = makeIo();
const project = projectWithConfig(withManagedIngestEmbedding(buildDefaultKtxProjectConfig()));
const loadProject = vi.fn(async () => project);
const ensureLocalEmbeddings = vi.fn(async () => fakeDaemon);
const result = await loadKtxCliProject(
{ projectDir: '/work/proj', cliVersion: '0.2.0', installPolicy: 'never', io: io.io },
{ loadProject, ensureLocalEmbeddings },
);
expect(ensureLocalEmbeddings).toHaveBeenCalledWith({
cliVersion: '0.2.0',
projectDir: '/work/proj',
installPolicy: 'never',
io: io.io,
});
expect(result.config.ingest.embeddings.sentenceTransformers?.base_url).toBe(RESOLVED_BASE_URL);
});
it('does not mutate process.env', async () => {
const io = makeIo();
const before = process.env.KTX_MANAGED_SENTENCE_TRANSFORMERS_BASE_URL;
delete process.env.KTX_MANAGED_SENTENCE_TRANSFORMERS_BASE_URL;
try {
const project = projectWithConfig(withManagedIngestEmbedding(buildDefaultKtxProjectConfig()));
await loadKtxCliProject(
{ projectDir: '/work/proj', cliVersion: '0.2.0', installPolicy: 'never', io: io.io },
{ loadProject: vi.fn(async () => project), ensureLocalEmbeddings: vi.fn(async () => fakeDaemon) },
);
expect(process.env.KTX_MANAGED_SENTENCE_TRANSFORMERS_BASE_URL).toBeUndefined();
} finally {
if (before === undefined) {
delete process.env.KTX_MANAGED_SENTENCE_TRANSFORMERS_BASE_URL;
} else {
process.env.KTX_MANAGED_SENTENCE_TRANSFORMERS_BASE_URL = before;
}
}
});
});

View file

@ -0,0 +1,91 @@
import { MANAGED_SENTENCE_TRANSFORMERS_BASE_URL } from '@ktx/context';
import { loadKtxProject, type KtxLocalProject } from '@ktx/context/project';
import type { KtxProjectConfig, KtxProjectEmbeddingConfig } from '@ktx/context/project';
import type { KtxCliIo } from './cli-runtime.js';
import {
ensureManagedLocalEmbeddingsDaemon,
type ManagedLocalEmbeddingsDaemon,
} from './managed-local-embeddings.js';
import type { KtxManagedPythonInstallPolicy } from './managed-python-command.js';
export interface LoadKtxCliProjectOptions {
projectDir: string;
cliVersion: string;
installPolicy: KtxManagedPythonInstallPolicy;
io: KtxCliIo;
}
export interface LoadKtxCliProjectDeps {
loadProject?: typeof loadKtxProject;
ensureLocalEmbeddings?: (
options: Parameters<typeof ensureManagedLocalEmbeddingsDaemon>[0],
) => Promise<ManagedLocalEmbeddingsDaemon>;
}
export async function loadKtxCliProject(
options: LoadKtxCliProjectOptions,
deps: LoadKtxCliProjectDeps = {},
): Promise<KtxLocalProject> {
const loadProject = deps.loadProject ?? loadKtxProject;
const ensureLocalEmbeddings = deps.ensureLocalEmbeddings ?? ensureManagedLocalEmbeddingsDaemon;
const project = await loadProject({ projectDir: options.projectDir });
if (!projectNeedsManagedLocalEmbeddings(project.config)) {
return project;
}
const daemon = await ensureLocalEmbeddings({
cliVersion: options.cliVersion,
projectDir: options.projectDir,
installPolicy: options.installPolicy,
io: options.io,
});
return {
...project,
config: substituteManagedLocalEmbeddingsUrl(project.config, daemon.baseUrl),
};
}
export function projectNeedsManagedLocalEmbeddings(config: KtxProjectConfig): boolean {
return (
embeddingUsesManagedSentinel(config.ingest.embeddings) ||
embeddingUsesManagedSentinel(config.scan.enrichment.embeddings)
);
}
export function substituteManagedLocalEmbeddingsUrl(
config: KtxProjectConfig,
baseUrl: string,
): KtxProjectConfig {
const ingestEmbeddings = rewriteManagedEmbeddingConfig(config.ingest.embeddings, baseUrl);
const scanEnrichmentEmbeddings = rewriteManagedEmbeddingConfig(config.scan.enrichment.embeddings, baseUrl);
return {
...config,
ingest: { ...config.ingest, embeddings: ingestEmbeddings },
scan: {
...config.scan,
enrichment: { ...config.scan.enrichment, embeddings: scanEnrichmentEmbeddings },
},
};
}
function embeddingUsesManagedSentinel(embedding: KtxProjectEmbeddingConfig | undefined): boolean {
return embedding?.sentenceTransformers?.base_url === MANAGED_SENTENCE_TRANSFORMERS_BASE_URL;
}
function rewriteManagedEmbeddingConfig<T extends KtxProjectEmbeddingConfig | undefined>(
embedding: T,
baseUrl: string,
): T {
if (!embedding || !embeddingUsesManagedSentinel(embedding)) {
return embedding;
}
return {
...embedding,
sentenceTransformers: {
...embedding.sentenceTransformers,
base_url: baseUrl,
},
} as T;
}

View file

@ -18,7 +18,8 @@ import {
sanitizeMemoryFlowError,
} from '@ktx/context/ingest';
import type { KtxSqlQueryExecutorPort } from '@ktx/context/connections';
import { loadKtxProject, type KtxLocalProject } from '@ktx/context/project';
import { type KtxLocalProject } from '@ktx/context/project';
import { loadKtxCliProject } from './cli-project.js';
import { createKtxCliIngestQueryExecutor } from './ingest-query-executor.js';
import { readIngestReportSnapshotFile } from './ingest-report-file.js';
import { createCliOperationalLogger } from './io/logger.js';
@ -529,7 +530,7 @@ function assertReportMatchesReplayId(report: IngestReportSnapshot, requestedId:
}
async function readStoredIngestReport(
project: Awaited<ReturnType<typeof loadKtxProject>>,
project: KtxLocalProject,
runId: string | undefined,
): Promise<IngestReportSnapshot | null> {
return runId ? await getLocalIngestStatus(project, runId) : await getLatestLocalIngestStatus(project);
@ -681,7 +682,14 @@ export async function runKtxIngest(
deps: KtxIngestDeps = {},
): Promise<number> {
try {
const project = await loadKtxProject({ projectDir: args.projectDir });
const cliVersion = args.command === 'run' ? args.cliVersion : undefined;
const runtimeInstallPolicy = args.command === 'run' ? args.runtimeInstallPolicy : undefined;
const project = await loadKtxCliProject({
projectDir: args.projectDir,
cliVersion: cliVersion ?? '0.0.0-private',
installPolicy: runtimeInstallPolicy ?? 'never',
io,
});
const env = deps.env ?? process.env;
if (args.command === 'run') {
const ingestProject =

View file

@ -1,8 +1,5 @@
import { describe, expect, it, vi } from 'vitest';
import {
MANAGED_SENTENCE_TRANSFORMERS_BASE_URL,
MANAGED_SENTENCE_TRANSFORMERS_BASE_URL_ENV,
} from '@ktx/context';
import { MANAGED_SENTENCE_TRANSFORMERS_BASE_URL } from '@ktx/context';
import {
ensureManagedLocalEmbeddingsDaemon,
managedLocalEmbeddingHealthConfig,
@ -152,9 +149,6 @@ describe('ensureManagedLocalEmbeddingsDaemon', () => {
baseUrl: 'http://127.0.0.1:61234',
stdoutLog: '/work/proj/.ktx/runtime/daemon.stdout.log',
stderrLog: '/work/proj/.ktx/runtime/daemon.stderr.log',
env: {
[MANAGED_SENTENCE_TRANSFORMERS_BASE_URL_ENV]: 'http://127.0.0.1:61234',
},
});
expect(ensureRuntime).toHaveBeenCalledWith({

View file

@ -1,7 +1,4 @@
import {
MANAGED_SENTENCE_TRANSFORMERS_BASE_URL,
MANAGED_SENTENCE_TRANSFORMERS_BASE_URL_ENV,
} from '@ktx/context';
import { MANAGED_SENTENCE_TRANSFORMERS_BASE_URL } from '@ktx/context';
import type { KtxProjectEmbeddingConfig } from '@ktx/context/project';
import type { KtxEmbeddingConfig } from '@ktx/llm';
import type { KtxCliIo } from './cli-runtime.js';
@ -16,7 +13,6 @@ export interface ManagedLocalEmbeddingsDaemon {
baseUrl: string;
stdoutLog: string;
stderrLog: string;
env: Record<typeof MANAGED_SENTENCE_TRANSFORMERS_BASE_URL_ENV, string>;
}
export interface ManagedLocalEmbeddingsOptions {
@ -95,8 +91,5 @@ export async function ensureManagedLocalEmbeddingsDaemon(
baseUrl: daemon.baseUrl,
stdoutLog: daemon.state.stdoutLog,
stderrLog: daemon.state.stderrLog,
env: {
[MANAGED_SENTENCE_TRANSFORMERS_BASE_URL_ENV]: daemon.baseUrl,
},
};
}

View file

@ -1,4 +1,5 @@
import { type KtxLocalProject, type KtxProjectConnectionConfig, loadKtxProject } from '@ktx/context/project';
import { type KtxLocalProject, type KtxProjectConnectionConfig } from '@ktx/context/project';
import { loadKtxCliProject } from './cli-project.js';
import type { KtxProgressPort } from '@ktx/context/scan';
import type { KtxCliIo } from './index.js';
import type { KtxIngestArgs, KtxIngestDeps, KtxIngestProgressUpdate } from './ingest.js';
@ -90,7 +91,7 @@ export type KtxPublicIngestProject = Pick<KtxLocalProject, 'projectDir' | 'confi
type KtxPublicIngestPhaseKey = 'database-schema' | 'query-history' | 'source-ingest';
export interface KtxPublicIngestDeps {
loadProject?: (options: Parameters<typeof loadKtxProject>[0]) => Promise<KtxPublicIngestProject>;
loadProject?: (options: { projectDir: string }) => Promise<KtxPublicIngestProject>;
runScan?: (args: KtxScanArgs, io: KtxCliIo, deps?: KtxScanDeps) => Promise<number>;
runIngest?: (args: KtxIngestArgs, io: KtxCliIo, deps?: KtxIngestDeps) => Promise<number>;
runContextBuild?: (
@ -867,7 +868,15 @@ export async function runKtxPublicIngest(
io: KtxCliIo,
deps: KtxPublicIngestDeps = {},
): Promise<number> {
const loadProject = deps.loadProject ?? loadKtxProject;
const loadProject =
deps.loadProject ??
((options: { projectDir: string }) =>
loadKtxCliProject({
projectDir: options.projectDir,
cliVersion: args.cliVersion ?? '0.0.0-private',
installPolicy: args.runtimeInstallPolicy ?? 'never',
io,
}));
const project = await loadProject({ projectDir: args.projectDir });
if (shouldUseForegroundContextBuildView(args, io)) {
const plan = buildPublicIngestPlan(project, args);

View file

@ -1,4 +1,3 @@
import { loadKtxProject } from '@ktx/context/project';
import {
type KtxProgressPort,
type KtxScanMode,
@ -6,6 +5,7 @@ import {
type KtxScanWarning,
runLocalScan,
} from '@ktx/context/scan';
import { loadKtxCliProject } from './cli-project.js';
import type { KtxCliIo } from './index.js';
import { createKtxCliLocalIngestAdapters } from './local-adapters.js';
import { createKtxCliScanConnector } from './local-scan-connectors.js';
@ -313,7 +313,12 @@ export function createCliScanProgress(
export async function runKtxScan(args: KtxScanArgs, io: KtxCliIo = process, deps: KtxScanDeps = {}): Promise<number> {
try {
const project = await loadKtxProject({ projectDir: args.projectDir });
const project = await loadKtxCliProject({
projectDir: args.projectDir,
cliVersion: args.cliVersion ?? '0.0.0-private',
installPolicy: args.runtimeInstallPolicy ?? 'never',
io,
});
const managedDaemon = managedDaemonOptionsForScanRun(args, deps.runtimeIo ?? io);
const connector =
args.mode !== 'structural' || args.detectRelationships

View file

@ -38,7 +38,6 @@ export {
} from './debug-request-recorder.js';
export {
MANAGED_SENTENCE_TRANSFORMERS_BASE_URL,
MANAGED_SENTENCE_TRANSFORMERS_BASE_URL_ENV,
createLocalKtxEmbeddingProviderFromConfig,
createLocalKtxLlmProviderFromConfig,
createLocalKtxLlmRuntimeFromConfig,

View file

@ -6,7 +6,6 @@ import {
} from '../project/config.js';
import {
MANAGED_SENTENCE_TRANSFORMERS_BASE_URL,
MANAGED_SENTENCE_TRANSFORMERS_BASE_URL_ENV,
createLocalKtxEmbeddingProviderFromConfig,
createLocalKtxLlmProviderFromConfig,
resolveLocalKtxEmbeddingConfig,
@ -152,32 +151,7 @@ describe('local KTX embedding config', () => {
});
});
it('resolves managed sentence-transformers config from the CLI-provided daemon URL', () => {
const config: KtxProjectEmbeddingConfig = {
backend: 'sentence-transformers',
model: 'all-MiniLM-L6-v2',
dimensions: 384,
sentenceTransformers: {
base_url: MANAGED_SENTENCE_TRANSFORMERS_BASE_URL,
pathPrefix: '',
},
batchSize: 32,
};
expect(
resolveLocalKtxEmbeddingConfig(config, {
[MANAGED_SENTENCE_TRANSFORMERS_BASE_URL_ENV]: 'http://127.0.0.1:61234',
}),
).toEqual({
backend: 'sentence-transformers',
model: 'all-MiniLM-L6-v2',
dimensions: 384,
sentenceTransformers: { baseURL: 'http://127.0.0.1:61234', pathPrefix: '' },
batchSize: 32,
});
});
it('returns null for managed sentence-transformers when no daemon URL is available', () => {
it('returns null when sentence-transformers base_url is still the unresolved managed sentinel', () => {
const config: KtxProjectEmbeddingConfig = {
backend: 'sentence-transformers',
model: 'all-MiniLM-L6-v2',

View file

@ -23,7 +23,6 @@ interface LocalConfigDeps {
}
export const MANAGED_SENTENCE_TRANSFORMERS_BASE_URL = 'managed:local-embeddings';
export const MANAGED_SENTENCE_TRANSFORMERS_BASE_URL_ENV = 'KTX_MANAGED_SENTENCE_TRANSFORMERS_BASE_URL';
function resolveOptional(value: string | undefined, env: NodeJS.ProcessEnv): string | undefined {
return resolveKtxConfigReference(value, env) || undefined;
@ -141,19 +140,6 @@ export function createLocalKtxLlmRuntimeFromConfig(
return (deps.createAiSdkRuntime ?? ((runtimeDeps) => new AiSdkKtxLlmRuntime(runtimeDeps)))({ llmProvider });
}
function resolveSentenceTransformersBaseUrl(
value: string | undefined,
env: NodeJS.ProcessEnv,
): string | undefined {
if (!value) {
return undefined;
}
if (value === MANAGED_SENTENCE_TRANSFORMERS_BASE_URL) {
return resolveOptional(`env:${MANAGED_SENTENCE_TRANSFORMERS_BASE_URL_ENV}`, env);
}
return value;
}
export function resolveLocalKtxEmbeddingConfig(
config: KtxProjectEmbeddingConfig,
env: NodeJS.ProcessEnv,
@ -162,8 +148,8 @@ export function resolveLocalKtxEmbeddingConfig(
return null;
}
if (config.backend === 'sentence-transformers') {
const baseURL = resolveSentenceTransformersBaseUrl(config.sentenceTransformers?.base_url, env);
if (!baseURL) {
const baseURL = config.sentenceTransformers?.base_url;
if (!baseURL || baseURL === MANAGED_SENTENCE_TRANSFORMERS_BASE_URL) {
return null;
}
return {

View file

@ -132,9 +132,6 @@ describe('@ktx/context package exports', () => {
expect(root.assertSearchBackendCapabilities).toBeTypeOf('function');
expect(root.createLocalKtxEmbeddingProviderFromConfig).toBeTypeOf('function');
expect(root.MANAGED_SENTENCE_TRANSFORMERS_BASE_URL).toBe('managed:local-embeddings');
expect(root.MANAGED_SENTENCE_TRANSFORMERS_BASE_URL_ENV).toBe(
'KTX_MANAGED_SENTENCE_TRANSFORMERS_BASE_URL',
);
expect(agent).toBeDefined();
expect(agent.AgentRunnerService).toBeTypeOf('function');
expect(root.AgentRunnerService).toBeTypeOf('function');