src / config / ConfigurationService.ts
src / config / ConfigurationService.ts
import { ImageProviderRegistry } from "../image_search/providers/ImageProviderRegistry";
import {
defaultProviderInstanceRegistry,
ProviderInstanceRegistry,
} from "../providers/ProviderInstanceRegistry";
import { ProviderRegistry } from "../providers/ProviderRegistry";
import { Logger } from "../utils/Logger";
import {
DEFAULT_READ_WEBPAGE_CHUNK_SIZE,
DEFAULT_SEARCH_MAX_PAYLOAD_CHARACTERS,
DEFAULT_SEARCH_MAX_SNIPPET_LENGTH,
} from "./Config";
import { IConfigurationSource } from "./IConfigurationSource";
import { EnvironmentConfigurationSource } from "./sources/EnvironmentConfigurationSource";
import { LMStudioConfigurationSource } from "./sources/LMStudioConfigurationSource";
export interface ConfigurationServiceOptions {
sources?: IConfigurationSource[];
}
/**
* Centralized ConfigurationService combining multiple IConfigurationSource providers in priority order.
* By default, reads LMStudioConfigurationSource first, falling back to EnvironmentConfigurationSource.
*/
export class ConfigurationService {
private sources: IConfigurationSource[];
public readonly lmStudioSource: LMStudioConfigurationSource;
public readonly envSource: EnvironmentConfigurationSource;
private lastConfigFingerprint: string = "";
private lastProviderKeySnapshots: Map<string, string> = new Map();
constructor(options: ConfigurationServiceOptions = {}) {
this.lmStudioSource = new LMStudioConfigurationSource();
this.envSource = new EnvironmentConfigurationSource();
this.sources = options.sources ?? [this.lmStudioSource, this.envSource];
}
public registerSource(source: IConfigurationSource): void {
this.sources.unshift(source);
}
public getWebDefaultProvider(): string {
for (const source of this.sources) {
const val = source.getWebDefaultProvider();
if (val) return val;
}
return "auto";
}
public getWebProviderPriority(): string[] {
for (const source of this.sources) {
const priority = source.getWebProviderPriority();
if (priority !== undefined) return priority;
}
// Default fallback priority from ProviderRegistry
return ProviderRegistry.get_all_specs().map((s) => s.name);
}
public getProviderKeys(providerName: string, category?: string): string[] {
for (const source of this.sources) {
const keys = source.getProviderKeys(providerName, category);
if (keys && keys.length > 0) return keys;
}
return [];
}
/**
* Snapshot-based synchronization of all registered provider instances in ProviderInstanceRegistry.
* Executed strictly during configuration lifecycle events. Uses dirty-checking to eliminate log spam
* and preserves KeyManager runtime state (cooldowns, rotation index, invalid keys) unless a provider's
* configured ordered keys actually changed.
*/
public syncAllProviders(
registry: ProviderInstanceRegistry = defaultProviderInstanceRegistry
): void {
const instances = registry.getAll();
const currentFingerprint = instances
.map((p) => {
const keys = this.getProviderKeys(p.providerName, p.category);
return `${p.configIdentity}:${keys.join(",")}`;
})
.join("|") + `|webDef:${this.getWebDefaultProvider()}|imgDef:${this.getImageDefaultProvider()}`;
const isChanged = currentFingerprint !== this.lastConfigFingerprint;
this.lastConfigFingerprint = currentFingerprint;
if (isChanged) {
Logger.info("[Config]", "Configuration updated.");
}
for (const provider of instances) {
const identity = provider.configIdentity.toLowerCase().trim();
const keys = this.getProviderKeys(provider.providerName, provider.category);
const keySnapshot = JSON.stringify(keys);
const previousSnapshot = this.lastProviderKeySnapshots.get(identity);
if (previousSnapshot === undefined || previousSnapshot !== keySnapshot) {
provider.setKeys(keys);
this.lastProviderKeySnapshots.set(identity, keySnapshot);
if (isChanged) {
const keyCountStr = `${keys.length} API key${keys.length === 1 ? "" : "s"}`;
Logger.info("[Config]", ` ${provider.configIdentity.padEnd(16, " ")} -> ${keyCountStr}`);
}
}
}
}
/**
* Compatibility alias for syncAllProviders.
*/
public syncProviderKeys(providers: { providerName: string; category?: string; setKeys: (keys: string[]) => void }[]): void {
for (const provider of providers) {
const keys = this.getProviderKeys(provider.providerName, provider.category);
provider.setKeys(keys);
}
}
public getImageDefaultProvider(): string {
for (const source of this.sources) {
const val = source.getImageDefaultProvider();
if (val) return val;
}
return "duckduckgo";
}
public getImageProviderPriority(): string[] {
for (const source of this.sources) {
const priority = source.getImageProviderPriority();
if (priority !== undefined) return priority;
}
// Default fallback priority from ImageProviderRegistry
return ImageProviderRegistry.getAllSpecs().map((s) => s.name);
}
public getHttpTimeoutSeconds(): number {
for (const source of this.sources) {
const timeout = source.getHttpTimeoutSeconds();
if (timeout !== undefined && timeout > 0) return timeout;
}
return 30.0;
}
public getQuotaCooldownHours(): number {
for (const source of this.sources) {
const cooldown = source.getQuotaCooldownHours();
if (cooldown !== undefined && cooldown > 0) return cooldown;
}
return 24.0;
}
public getSearchMaxSnippetLength(): number {
for (const source of this.sources) {
const val = source.getSearchMaxSnippetLength?.();
if (val !== undefined && val > 0) return val;
}
return DEFAULT_SEARCH_MAX_SNIPPET_LENGTH;
}
public getSearchMaxPayloadCharacters(): number {
for (const source of this.sources) {
const val = source.getSearchMaxPayloadCharacters?.();
if (val !== undefined && val > 0) return val;
}
return DEFAULT_SEARCH_MAX_PAYLOAD_CHARACTERS;
}
public getPrependTimeContext(): boolean {
for (const source of this.sources) {
const val = source.getPrependTimeContext?.();
if (val !== undefined) return val;
}
return true;
}
public getPrependSearchImagesContext(): boolean {
for (const source of this.sources) {
const val = source.getPrependSearchImagesContext?.();
if (val !== undefined) return val;
}
return true;
}
public getPrependVisionContext(): boolean {
for (const source of this.sources) {
const val = source.getPrependVisionContext?.();
if (val !== undefined) return val;
}
return true;
}
public getMaxImageResults(): number {
for (const source of this.sources) {
const val = source.getMaxImageResults?.();
if (val !== undefined && val > 0) return val;
}
return 10;
}
public getMaxWebResults(): number {
for (const source of this.sources) {
const val = source.getMaxWebResults?.();
if (val !== undefined && val > 0) return val;
}
return 10;
}
public getReadWebpageChunkSize(): number {
for (const source of this.sources) {
const val = source.getReadWebpageChunkSize?.();
if (val !== undefined && val > 0) return val;
}
return DEFAULT_READ_WEBPAGE_CHUNK_SIZE;
}
}
export const defaultConfigurationService = new ConfigurationService();
import { ImageProviderRegistry } from "../image_search/providers/ImageProviderRegistry";
import {
defaultProviderInstanceRegistry,
ProviderInstanceRegistry,
} from "../providers/ProviderInstanceRegistry";
import { ProviderRegistry } from "../providers/ProviderRegistry";
import { Logger } from "../utils/Logger";
import {
DEFAULT_READ_WEBPAGE_CHUNK_SIZE,
DEFAULT_SEARCH_MAX_PAYLOAD_CHARACTERS,
DEFAULT_SEARCH_MAX_SNIPPET_LENGTH,
} from "./Config";
import { IConfigurationSource } from "./IConfigurationSource";
import { EnvironmentConfigurationSource } from "./sources/EnvironmentConfigurationSource";
import { LMStudioConfigurationSource } from "./sources/LMStudioConfigurationSource";
export interface ConfigurationServiceOptions {
sources?: IConfigurationSource[];
}
/**
* Centralized ConfigurationService combining multiple IConfigurationSource providers in priority order.
* By default, reads LMStudioConfigurationSource first, falling back to EnvironmentConfigurationSource.
*/
export class ConfigurationService {
private sources: IConfigurationSource[];
public readonly lmStudioSource: LMStudioConfigurationSource;
public readonly envSource: EnvironmentConfigurationSource;
private lastConfigFingerprint: string = "";
private lastProviderKeySnapshots: Map<string, string> = new Map();
constructor(options: ConfigurationServiceOptions = {}) {
this.lmStudioSource = new LMStudioConfigurationSource();
this.envSource = new EnvironmentConfigurationSource();
this.sources = options.sources ?? [this.lmStudioSource, this.envSource];
}
public registerSource(source: IConfigurationSource): void {
this.sources.unshift(source);
}
public getWebDefaultProvider(): string {
for (const source of this.sources) {
const val = source.getWebDefaultProvider();
if (val) return val;
}
return "auto";
}
public getWebProviderPriority(): string[] {
for (const source of this.sources) {
const priority = source.getWebProviderPriority();
if (priority !== undefined) return priority;
}
// Default fallback priority from ProviderRegistry
return ProviderRegistry.get_all_specs().map((s) => s.name);
}
public getProviderKeys(providerName: string, category?: string): string[] {
for (const source of this.sources) {
const keys = source.getProviderKeys(providerName, category);
if (keys && keys.length > 0) return keys;
}
return [];
}
/**
* Snapshot-based synchronization of all registered provider instances in ProviderInstanceRegistry.
* Executed strictly during configuration lifecycle events. Uses dirty-checking to eliminate log spam
* and preserves KeyManager runtime state (cooldowns, rotation index, invalid keys) unless a provider's
* configured ordered keys actually changed.
*/
public syncAllProviders(
registry: ProviderInstanceRegistry = defaultProviderInstanceRegistry
): void {
const instances = registry.getAll();
const currentFingerprint = instances
.map((p) => {
const keys = this.getProviderKeys(p.providerName, p.category);
return `${p.configIdentity}:${keys.join(",")}`;
})
.join("|") + `|webDef:${this.getWebDefaultProvider()}|imgDef:${this.getImageDefaultProvider()}`;
const isChanged = currentFingerprint !== this.lastConfigFingerprint;
this.lastConfigFingerprint = currentFingerprint;
if (isChanged) {
Logger.info("[Config]", "Configuration updated.");
}
for (const provider of instances) {
const identity = provider.configIdentity.toLowerCase().trim();
const keys = this.getProviderKeys(provider.providerName, provider.category);
const keySnapshot = JSON.stringify(keys);
const previousSnapshot = this.lastProviderKeySnapshots.get(identity);
if (previousSnapshot === undefined || previousSnapshot !== keySnapshot) {
provider.setKeys(keys);
this.lastProviderKeySnapshots.set(identity, keySnapshot);
if (isChanged) {
const keyCountStr = `${keys.length} API key${keys.length === 1 ? "" : "s"}`;
Logger.info("[Config]", ` ${provider.configIdentity.padEnd(16, " ")} -> ${keyCountStr}`);
}
}
}
}
/**
* Compatibility alias for syncAllProviders.
*/
public syncProviderKeys(providers: { providerName: string; category?: string; setKeys: (keys: string[]) => void }[]): void {
for (const provider of providers) {
const keys = this.getProviderKeys(provider.providerName, provider.category);
provider.setKeys(keys);
}
}
public getImageDefaultProvider(): string {
for (const source of this.sources) {
const val = source.getImageDefaultProvider();
if (val) return val;
}
return "duckduckgo";
}
public getImageProviderPriority(): string[] {
for (const source of this.sources) {
const priority = source.getImageProviderPriority();
if (priority !== undefined) return priority;
}
// Default fallback priority from ImageProviderRegistry
return ImageProviderRegistry.getAllSpecs().map((s) => s.name);
}
public getHttpTimeoutSeconds(): number {
for (const source of this.sources) {
const timeout = source.getHttpTimeoutSeconds();
if (timeout !== undefined && timeout > 0) return timeout;
}
return 30.0;
}
public getQuotaCooldownHours(): number {
for (const source of this.sources) {
const cooldown = source.getQuotaCooldownHours();
if (cooldown !== undefined && cooldown > 0) return cooldown;
}
return 24.0;
}
public getSearchMaxSnippetLength(): number {
for (const source of this.sources) {
const val = source.getSearchMaxSnippetLength?.();
if (val !== undefined && val > 0) return val;
}
return DEFAULT_SEARCH_MAX_SNIPPET_LENGTH;
}
public getSearchMaxPayloadCharacters(): number {
for (const source of this.sources) {
const val = source.getSearchMaxPayloadCharacters?.();
if (val !== undefined && val > 0) return val;
}
return DEFAULT_SEARCH_MAX_PAYLOAD_CHARACTERS;
}
public getPrependTimeContext(): boolean {
for (const source of this.sources) {
const val = source.getPrependTimeContext?.();
if (val !== undefined) return val;
}
return true;
}
public getPrependSearchImagesContext(): boolean {
for (const source of this.sources) {
const val = source.getPrependSearchImagesContext?.();
if (val !== undefined) return val;
}
return true;
}
public getPrependVisionContext(): boolean {
for (const source of this.sources) {
const val = source.getPrependVisionContext?.();
if (val !== undefined) return val;
}
return true;
}
public getMaxImageResults(): number {
for (const source of this.sources) {
const val = source.getMaxImageResults?.();
if (val !== undefined && val > 0) return val;
}
return 10;
}
public getMaxWebResults(): number {
for (const source of this.sources) {
const val = source.getMaxWebResults?.();
if (val !== undefined && val > 0) return val;
}
return 10;
}
public getReadWebpageChunkSize(): number {
for (const source of this.sources) {
const val = source.getReadWebpageChunkSize?.();
if (val !== undefined && val > 0) return val;
}
return DEFAULT_READ_WEBPAGE_CHUNK_SIZE;
}
}
export const defaultConfigurationService = new ConfigurationService();