fix(cli): resolve embedding provider explicitly and surface lane status in sl search (#192)

* feat(cli): add tryUseManagedLocalEmbeddingsDaemon for read-only callers

* feat(cli): add resolveProjectEmbeddingProvider helper

* fix(cli): wire sl search through resolveProjectEmbeddingProvider so semantic lane works

* fix(cli): wire wiki/knowledge search through resolveProjectEmbeddingProvider

* feat(cli): surface embeddings-unavailable status when sl search returns empty

* refactor(cli): route admin reindex through resolveProjectEmbeddingProvider

* refactor: pass embeddingProvider into ingest/scan instead of resolving inside @ktx/context

* refactor(mcp): resolve embedding provider in CLI factory, pass into context ports

* refactor(context): delete MANAGED_SENTENCE_TRANSFORMERS_BASE_URL sentinel

* refactor(cli): delete sentinel-based managed-embeddings indirection

* chore: scrub stale managed-embeddings sentinel references from tests and smoke script

* chore: unexport unused EmbeddingResolutionMode alias

* fix(cli): force pathPrefix="" when targeting the managed embeddings daemon

The managed daemon serves /embeddings/compute directly. The default
pathPrefix in @ktx/llm is /api, so omitting sentenceTransformers from
ktx.yaml produced /api/embeddings/compute -> 404. The resolver now
sets pathPrefix='' explicitly when wiring the managed daemon URL,
matching what the daemon actually exposes.
This commit is contained in:
Andrey Avtomonov 2026-05-21 02:21:22 +02:00 committed by GitHub
parent 56a967278a
commit 9d92c79988
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
36 changed files with 750 additions and 442 deletions

View file

@ -1,91 +1,20 @@
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>;
}
/**
* Thin wrapper around `loadKtxProject`. Kept as a single entrypoint so the CLI can grow shared
* pre-load behavior later (telemetry, project lock, etc.). Today it does no extra work.
*/
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;
return (deps.loadProject ?? loadKtxProject)({ projectDir: options.projectDir });
}