import { type KnowledgeSpaceEmbeddingProfile as CoreKnowledgeSpaceEmbeddingProfile, KnowledgeSpaceEmbeddingProfileSchema, } from "@knowledge/core"; import type { EmbeddingProvider } from "@knowledge/embeddings"; import { type KnowledgeSpaceManifestRepository, observeKnowledgeSpaceEmbeddingDimension, } from "./knowledge-space-manifest-repository"; /** * Stable identity of a knowledge space's text-embedding vector space. * * `model` is the Dify-managed invocation model. `vectorSpaceId` is the immutable identity stored * on projections and used by retrieval. A runtime response must echo the configured model exactly; * accepting an unpersisted alias could silently change the vector semantics behind this identity. */ export type KnowledgeSpaceEmbeddingProfile = CoreKnowledgeSpaceEmbeddingProfile; export interface ResolveKnowledgeSpaceEmbeddingInput { /** Frozen active profile captured with the publication head; skips a mutable manifest reread. */ readonly profile?: KnowledgeSpaceEmbeddingProfile | undefined; readonly knowledgeSpaceId: string; readonly tenantId: string; } export interface ResolvedKnowledgeSpaceEmbedding extends KnowledgeSpaceEmbeddingProfile { readonly providerInstance: EmbeddingProvider; } export interface KnowledgeSpaceEmbeddingResolver { observeDimension?(input: ObserveResolvedKnowledgeSpaceEmbeddingDimensionInput): Promise; resolve( input: ResolveKnowledgeSpaceEmbeddingInput, ): Promise; } export interface ObserveResolvedKnowledgeSpaceEmbeddingDimensionInput extends ResolveKnowledgeSpaceEmbeddingInput { readonly dimension: number; readonly revision: number; readonly vectorSpaceId: string; } export type KnowledgeSpaceEmbeddingProviderFactory = ( profile: KnowledgeSpaceEmbeddingProfile, ) => EmbeddingProvider; export interface KnowledgeSpaceEmbeddingResolverOptions { /** Optional deployment profile used only by legacy manifests without an embedding profile. */ readonly fallback?: | { readonly profile: KnowledgeSpaceEmbeddingProfile; readonly provider: EmbeddingProvider; } | undefined; readonly manifests: KnowledgeSpaceManifestRepository; /** Bounded LRU size for provider instances keyed by immutable embedding profiles. */ readonly maxCachedProviders?: number | undefined; readonly now?: (() => string) | undefined; readonly providerFactory: KnowledgeSpaceEmbeddingProviderFactory; } export class KnowledgeSpaceEmbeddingProfileNotFoundError extends Error { constructor(knowledgeSpaceId: string) { super(`Embedding profile is not configured for knowledge space ${knowledgeSpaceId}`); } } export class KnowledgeSpaceEmbeddingProfileChangedError extends Error { constructor(vectorSpaceId: string) { super(`Embedding profile changed while using vector space ${vectorSpaceId}`); } } export class InvalidKnowledgeSpaceEmbeddingProfileError extends Error {} /** * Resolves the active profile in tenant + knowledge-space scope and reuses provider instances only * for byte-for-byte identical profiles. Credentials are intentionally absent from the persisted * profile and remain provider-factory/runtime concerns. */ export function createKnowledgeSpaceEmbeddingResolver({ fallback, manifests, maxCachedProviders = 256, now = () => new Date().toISOString(), providerFactory, }: KnowledgeSpaceEmbeddingResolverOptions): KnowledgeSpaceEmbeddingResolver { if (!Number.isSafeInteger(maxCachedProviders) || maxCachedProviders < 1) { throw new Error("Knowledge space embedding resolver maxCachedProviders must be at least 1"); } const providerCache = new Map(); const parsedFallback = fallback ? { profile: validateKnowledgeSpaceEmbeddingProfile(fallback.profile), provider: fallback.provider, } : undefined; return { observeDimension: async ({ dimension, knowledgeSpaceId, revision, tenantId, vectorSpaceId, }) => { const normalizedKnowledgeSpaceId = requiredText(knowledgeSpaceId, "knowledgeSpaceId"); const normalizedTenantId = requiredText(tenantId, "tenantId"); const observed = await observeKnowledgeSpaceEmbeddingDimension(manifests, { dimension, expectedRevision: revision, expectedVectorSpaceId: vectorSpaceId, knowledgeSpaceId: normalizedKnowledgeSpaceId, now, tenantId: normalizedTenantId, }); if (observed) { return; } // Legacy manifests intentionally keep using the raw historical model as vectorSpaceId until // an explicit rebuild publishes a canonical profile. There is no persisted profile to update. const current = await manifests.get({ knowledgeSpaceId: normalizedKnowledgeSpaceId, tenantId: normalizedTenantId, }); if ( !current?.embeddingProfile && parsedFallback?.profile.vectorSpaceId === vectorSpaceId && parsedFallback.profile.revision === revision ) { assertObservedEmbeddingDimension({ observedDimension: dimension, profile: parsedFallback.profile, }); return; } throw new KnowledgeSpaceEmbeddingProfileChangedError(vectorSpaceId); }, resolve: async ({ knowledgeSpaceId, profile: frozenProfile, tenantId }) => { const normalizedKnowledgeSpaceId = requiredText(knowledgeSpaceId, "knowledgeSpaceId"); const normalizedTenantId = requiredText(tenantId, "tenantId"); const manifest = frozenProfile ? undefined : await manifests.get({ knowledgeSpaceId: normalizedKnowledgeSpaceId, tenantId: normalizedTenantId, }); if (!manifest && !frozenProfile) { if (parsedFallback) { return { ...parsedFallback.profile, providerInstance: parsedFallback.provider, }; } throw new KnowledgeSpaceEmbeddingProfileNotFoundError(normalizedKnowledgeSpaceId); } const rawProfile = frozenProfile ?? (manifest as unknown as { readonly embeddingProfile?: unknown }).embeddingProfile; if (rawProfile === undefined) { return parsedFallback ? { ...parsedFallback.profile, providerInstance: parsedFallback.provider, } : null; } // Persisted profiles have already crossed a trust boundary, so require the core schema's // canonical SHA-256 vector-space id. The looser parser is reserved for the internal legacy // fallback whose historical projection key is the raw model name. const profile = KnowledgeSpaceEmbeddingProfileSchema.parse(rawProfile); const cacheKey = embeddingProviderCacheKey(profile); const cachedProvider = providerCache.get(cacheKey); const provider = cachedProvider ?? providerFactory(profile); // Map insertion order provides a dependency-free LRU. Eviction only drops reusable runtime // state; routing credentials remain daemon-side and are never part of this cache key. if (cachedProvider) { providerCache.delete(cacheKey); } else if (providerCache.size >= maxCachedProviders) { const leastRecentlyUsed = providerCache.keys().next().value; if (leastRecentlyUsed !== undefined) { providerCache.delete(leastRecentlyUsed); } } providerCache.set(cacheKey, provider); return { ...profile, providerInstance: provider }; }, }; } export function validateKnowledgeSpaceEmbeddingProfile( profile: KnowledgeSpaceEmbeddingProfile, ): KnowledgeSpaceEmbeddingProfile { return parseKnowledgeSpaceEmbeddingProfile(profile); } export function assertObservedEmbeddingDimension({ observedDimension, profile, }: { readonly observedDimension: number; readonly profile: Pick; }): void { if (!Number.isSafeInteger(observedDimension) || observedDimension < 1) { throw new InvalidKnowledgeSpaceEmbeddingProfileError( `Embedding response for vector space ${profile.vectorSpaceId} has invalid dimension=${observedDimension}`, ); } if (profile.dimension !== undefined && profile.dimension !== observedDimension) { throw new InvalidKnowledgeSpaceEmbeddingProfileError( `Embedding response for vector space ${profile.vectorSpaceId} has dimension=${observedDimension}; ` + `expected observed dimension=${profile.dimension}`, ); } } export function assertEmbeddingModelMatchesProfile({ observedModel, profile, }: { readonly observedModel: string; readonly profile: Pick; }): void { const normalizedObservedModel = observedModel.trim(); if (!normalizedObservedModel) { throw new InvalidKnowledgeSpaceEmbeddingProfileError( `Embedding response for vector space ${profile.vectorSpaceId} has an empty model`, ); } if (normalizedObservedModel !== profile.model) { throw new InvalidKnowledgeSpaceEmbeddingProfileError( `Embedding response model ${normalizedObservedModel} does not match configured model ` + `${profile.model} for vector space ${profile.vectorSpaceId}`, ); } } function parseKnowledgeSpaceEmbeddingProfile(value: unknown): KnowledgeSpaceEmbeddingProfile { if (!isRecord(value)) { throw new InvalidKnowledgeSpaceEmbeddingProfileError("Embedding profile must be an object"); } const revisionValue = value.revision ?? value.version; const revision = typeof revisionValue === "number" && Number.isSafeInteger(revisionValue) && revisionValue > 0 ? revisionValue : undefined; if (revision === undefined) { throw new InvalidKnowledgeSpaceEmbeddingProfileError( "Embedding profile revision must be a positive integer", ); } const dimensionValue = value.dimension; const dimension = dimensionValue === undefined ? undefined : typeof dimensionValue === "number" && Number.isSafeInteger(dimensionValue) && dimensionValue > 0 ? dimensionValue : null; if (dimension === null) { throw new InvalidKnowledgeSpaceEmbeddingProfileError( "Embedding profile dimension must be a positive integer when present", ); } return { ...(dimension === undefined ? {} : { dimension }), model: requiredProfileText(value.model, "model"), pluginId: requiredProfileText(value.pluginId, "pluginId"), provider: requiredProfileText(value.provider, "provider"), revision, vectorSpaceId: requiredProfileText(value.vectorSpaceId, "vectorSpaceId"), }; } function requiredProfileText(value: unknown, name: string): string { if (typeof value !== "string" || !value.trim()) { throw new InvalidKnowledgeSpaceEmbeddingProfileError(`Embedding profile ${name} is required`); } return value.trim(); } function requiredText(value: string, name: string): string { const normalized = value.trim(); if (!normalized) { throw new Error(`Knowledge space embedding resolver ${name} is required`); } return normalized; } function embeddingProviderCacheKey(profile: KnowledgeSpaceEmbeddingProfile): string { return JSON.stringify([ profile.pluginId, profile.provider, profile.model, profile.vectorSpaceId, profile.revision, ]); } function isRecord(value: unknown): value is Readonly> { return value !== null && typeof value === "object" && !Array.isArray(value); }