import { OnModuleInit } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; import { ClsService } from "nestjs-cls"; import { BaseConfigInterface } from "../../../config/interfaces"; import type { AiConnection } from "../../../foundations/ai-connection/entities/ai-connection"; import { AiConnectionRepository } from "../../../foundations/ai-connection/repositories/ai-connection.repository"; import { AiConnectionEncryptionService } from "../../../foundations/ai-connection/services/ai-connection-encryption.service"; import { AppLoggingService } from "../../logging/services/logging.service"; import { AiConnectionType, ResolvedAiCandidate } from "../interfaces/ai-candidate.interface"; /** * Normalises one `AiConnection` node into the candidate shape `buildChatModel` * (and its embedder/transcriber siblings) already consume. * * Pure on purpose: the decryptor is injected so the caller decides what a * decryption failure means. It THROWS when a stored secret cannot be decrypted — * {@link AiConnectionResolverService.refreshNow} catches that and skips the * connection, so a single unreadable secret never takes the chain down * (spec § 5 "Error handling"). */ export declare function toCandidate(connection: AiConnection, decrypt: (value: string) => string): ResolvedAiCandidate; /** * Resolves the ordered fallback chain for an AI connection type. * * Holds an in-memory snapshot of every enabled `AiConnection` node (decrypted, * grouped into chains by `(connectionType, companyId | null)`, ordered by * `position`), refreshed at boot, every 60s, and immediately on any admin write * in this process (`AI_CONNECTIONS_CHANGED_EVENT`). * * The snapshot exists because `ModelService.getLLM()` is synchronous and called * directly by several library services: resolution must stay a pure sync lookup * with no signature ripple (spec § 2). * * Failure ALWAYS degrades toward today's `.env` behaviour, never toward "no AI" * (spec § 5): an empty table, a failed refresh, an undecryptable secret or an * unknown connection type all leave the `.env` candidate in place, and nothing * on the `resolve()` hot path throws. */ export declare class AiConnectionResolverService implements OnModuleInit { private readonly repository; private readonly encryption; private readonly cls; private readonly configService; private readonly logger?; /** `${connectionType}|${companyId ?? "global"}` → ordered candidates. */ private snapshot; /** connectionId → epoch ms before which the candidate is skipped. */ private readonly cooldownUntil; private refreshTimer?; constructor(repository: AiConnectionRepository, encryption: AiConnectionEncryptionService, cls: ClsService, configService: ConfigService, logger?: AppLoggingService); /** * Loads the first snapshot and starts the periodic refresh. Neither is * awaited and neither can reject: boot must never depend on the database * holding AI connection rows (spec § 5). */ onModuleInit(): void; /** * The ordered candidates to try for `type`, best first. * * Per-company chain when the request's company has one, otherwise the global * chain; the `.env` block is ALWAYS appended last. Candidates inside their * cooldown window are dropped — unless that empties the list, in which case * the full chain is returned anyway (fail-open, spec § 2). * * Never throws. */ resolve(type: AiConnectionType): ResolvedAiCandidate[]; /** * Parks a connection for `ai.connectionCooldownMinutes` after a transient * failure (429 / 5xx / network). The `.env` candidate is tracked under * `env:` like any other, so a dead env fallback cannot block recovery. */ markFailure(connectionId: string): void; /** * Rebuilds the snapshot from the database. Also the handler for admin writes * in this process; other processes catch up at the next scheduled refresh. * * A read failure keeps the PREVIOUS snapshot — a database blip must not wipe * working configuration (spec § 5). */ refreshNow(): Promise; /** * Reads every connection inside a CLS context: the refresh runs outside any * HTTP request, and the query plumbing reads CLS (`companyId`) unconditionally. */ private readConnections; /** * The company of the current request, or undefined when there is none. * * Optional-call plus a silent catch: the resolver is also used by workers and * boot-time code with no CLS context at all, which is an ordinary state and * must not log or throw — it simply means "use the global chain". */ private currentCompanyId; private chainKey; private get aiConfig(); private get cooldownMinutes(); /** * The `.env` block for a type, mapped field-for-field onto a candidate. This * is the final link of every chain and the whole behaviour when the table is * empty — with zero `AiConnection` nodes, resolution is byte-for-byte today's. */ private buildEnvCandidate; /** Same block mapping the admin API uses for `meta.envDefaults`. */ private envBlockFor; private warn; } //# sourceMappingURL=ai-connection-resolver.service.d.ts.map