Fix AI model registry staleness on self-hosted instances (#19427)

## Summary

Fixes #19422.

Self-hosted users hitting "No AI models are available" after configuring
API keys via the admin panel were victims of a stale
`AiModelRegistryService` cache. Only the four `addAiProvider` /
`removeAiProvider` / `addModelToProvider` / `removeModelFromProvider`
mutations called `refreshRegistry()` — setting an API key through
`set/update/deleteDatabaseConfigVariable` left the registry pointing at
the pre-mutation provider state.

Rather than patch each mutation site (and re-introduce the same class of
bug on the next one), the registry now invalidates lazily based on the
LLM config-group hash, mirroring the pattern `WebSearchDriverFactory`
already uses via `DriverFactoryBase`. Any mutation to an LLM-tagged
config variable is picked up automatically on the next read — callers
never have to remember to refresh.

- Extracted `getConfigGroupHash` into a shared util reused by both
`DriverFactoryBase` and `AiModelRegistryService` (and switched the hash
to `JSON.stringify` so object-typed config vars like `AI_PROVIDERS`
actually contribute meaningfully).
- `AiModelRegistryService` gates all internal `Map` access behind
private getters that call `ensureFresh()`, so future read paths can't
accidentally observe stale state. Build path uses underscored backing
fields directly to avoid recursing.
- Dropped the now-redundant `refreshRegistry()` public method and its
four call sites in `admin-panel.resolver.ts`.

## Test plan

- [x] `nx typecheck twenty-server` passes
- [x] Existing admin-panel + ai-models specs pass (79 tests)
- [ ] Manual: on a self-hosted instance, set `OPENAI_API_KEY` (or
another provider key) via the admin panel `setDatabaseConfigVariable`
mutation and confirm AI features work without a server restart
- [ ] Manual: existing flows (`addAiProvider`, `addModelToProvider`,
etc.) still pick up new providers on the next read

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Félix Malfait
2026-04-08 22:32:05 +02:00
committed by GitHub
parent 5874fb5f39
commit 6073bb6706
15 changed files with 152 additions and 64 deletions
@@ -403,7 +403,6 @@ export class AdminPanelResolver {
customProviders[providerName] = providerConfig;
await this.twentyConfigService.set('AI_PROVIDERS', customProviders);
this.aiModelRegistryService.refreshRegistry();
return true;
}
@@ -420,7 +419,6 @@ export class AdminPanelResolver {
delete customProviders[providerName];
await this.twentyConfigService.set('AI_PROVIDERS', customProviders);
this.aiModelRegistryService.refreshRegistry();
return true;
}
@@ -475,7 +473,6 @@ export class AdminPanelResolver {
};
await this.twentyConfigService.set('AI_PROVIDERS', customProviders);
this.aiModelRegistryService.refreshRegistry();
return true;
}
@@ -508,7 +505,6 @@ export class AdminPanelResolver {
};
await this.twentyConfigService.set('AI_PROVIDERS', customProviders);
this.aiModelRegistryService.refreshRegistry();
return true;
}
@@ -8,15 +8,17 @@ import { CaptchaDriverType } from 'src/engine/core-modules/captcha/interfaces';
import { SecureHttpClientService } from 'src/engine/core-modules/secure-http-client/secure-http-client.service';
import { DriverFactoryBase } from 'src/engine/core-modules/twenty-config/dynamic-factory.base';
import { ConfigVariablesGroup } from 'src/engine/core-modules/twenty-config/enums/config-variables-group.enum';
import { ConfigGroupHashService } from 'src/engine/core-modules/twenty-config/services/config-group-hash.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
@Injectable()
export class CaptchaDriverFactory extends DriverFactoryBase<CaptchaDriver | null> {
constructor(
twentyConfigService: TwentyConfigService,
configGroupHashService: ConfigGroupHashService,
private readonly secureHttpClientService: SecureHttpClientService,
) {
super(twentyConfigService);
super(twentyConfigService, configGroupHashService);
}
protected buildConfigKey(): string {
@@ -26,7 +28,7 @@ export class CaptchaDriverFactory extends DriverFactoryBase<CaptchaDriver | null
return 'disabled';
}
return `${driver}|${this.getConfigGroupHash(ConfigVariablesGroup.CAPTCHA_CONFIG)}`;
return `${driver}|${this.configGroupHashService.computeHash(ConfigVariablesGroup.CAPTCHA_CONFIG)}`;
}
protected createDriver(): CaptchaDriver | null {
@@ -9,19 +9,23 @@ import { LocalDriver } from 'src/engine/core-modules/code-interpreter/drivers/lo
import { NodeEnvironment } from 'src/engine/core-modules/twenty-config/interfaces/node-environment.interface';
import { DriverFactoryBase } from 'src/engine/core-modules/twenty-config/dynamic-factory.base';
import { ConfigVariablesGroup } from 'src/engine/core-modules/twenty-config/enums/config-variables-group.enum';
import { ConfigGroupHashService } from 'src/engine/core-modules/twenty-config/services/config-group-hash.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
@Injectable()
export class CodeInterpreterDriverFactory extends DriverFactoryBase<CodeInterpreterDriver> {
constructor(twentyConfigService: TwentyConfigService) {
super(twentyConfigService);
constructor(
twentyConfigService: TwentyConfigService,
configGroupHashService: ConfigGroupHashService,
) {
super(twentyConfigService, configGroupHashService);
}
protected buildConfigKey(): string {
const driverType = this.twentyConfigService.get('CODE_INTERPRETER_TYPE');
if (driverType === CodeInterpreterDriverType.E_2_B) {
return `e2b|${this.getConfigGroupHash(ConfigVariablesGroup.CODE_INTERPRETER_CONFIG)}`;
return `e2b|${this.configGroupHashService.computeHash(ConfigVariablesGroup.CODE_INTERPRETER_CONFIG)}`;
}
return driverType;
@@ -2,15 +2,20 @@ import { Test, type TestingModule } from '@nestjs/testing';
import { EmailDriverFactory } from 'src/engine/core-modules/email/email-driver.factory';
import { EmailDriver } from 'src/engine/core-modules/email/enums/email-driver.enum';
import { ConfigGroupHashService } from 'src/engine/core-modules/twenty-config/services/config-group-hash.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
describe('EmailDriverFactory', () => {
let factory: EmailDriverFactory;
let twentyConfigService: TwentyConfigService;
let configGroupHashService: ConfigGroupHashService;
const mockTwentyConfigService = {
get: jest.fn(),
};
const mockConfigGroupHashService = {
computeHash: jest.fn().mockReturnValue(''),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
@@ -20,11 +25,18 @@ describe('EmailDriverFactory', () => {
provide: TwentyConfigService,
useValue: mockTwentyConfigService,
},
{
provide: ConfigGroupHashService,
useValue: mockConfigGroupHashService,
},
],
}).compile();
factory = module.get<EmailDriverFactory>(EmailDriverFactory);
twentyConfigService = module.get<TwentyConfigService>(TwentyConfigService);
configGroupHashService = module.get<ConfigGroupHashService>(
ConfigGroupHashService,
);
jest.clearAllMocks();
});
@@ -44,7 +56,7 @@ describe('EmailDriverFactory', () => {
it('should return smtp config key for smtp driver', () => {
jest.spyOn(twentyConfigService, 'get').mockReturnValue(EmailDriver.SMTP);
jest
.spyOn(factory as any, 'getConfigGroupHash')
.spyOn(configGroupHashService, 'computeHash')
.mockReturnValue('smtp-hash-123');
const result = factory['buildConfigKey']();
@@ -179,7 +191,7 @@ describe('EmailDriverFactory', () => {
}
});
jest
.spyOn(factory as any, 'getConfigGroupHash')
.spyOn(configGroupHashService, 'computeHash')
.mockReturnValue('smtp-hash-123');
const driver2 = factory.getCurrentDriver();
@@ -214,7 +226,7 @@ describe('EmailDriverFactory', () => {
});
jest
.spyOn(factory as any, 'getConfigGroupHash')
.spyOn(configGroupHashService, 'computeHash')
.mockReturnValue('smtp-hash-123');
jest.spyOn(factory as any, 'createDriver').mockImplementation(() => {
@@ -7,12 +7,16 @@ import { SmtpDriver } from 'src/engine/core-modules/email/drivers/smtp.driver';
import { EmailDriver } from 'src/engine/core-modules/email/enums/email-driver.enum';
import { DriverFactoryBase } from 'src/engine/core-modules/twenty-config/dynamic-factory.base';
import { ConfigVariablesGroup } from 'src/engine/core-modules/twenty-config/enums/config-variables-group.enum';
import { ConfigGroupHashService } from 'src/engine/core-modules/twenty-config/services/config-group-hash.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
@Injectable()
export class EmailDriverFactory extends DriverFactoryBase<EmailDriverInterface> {
constructor(twentyConfigService: TwentyConfigService) {
super(twentyConfigService);
constructor(
twentyConfigService: TwentyConfigService,
configGroupHashService: ConfigGroupHashService,
) {
super(twentyConfigService, configGroupHashService);
}
protected buildConfigKey(): string {
@@ -23,7 +27,7 @@ export class EmailDriverFactory extends DriverFactoryBase<EmailDriverInterface>
}
if (driver === EmailDriver.SMTP) {
const emailConfigHash = this.getConfigGroupHash(
const emailConfigHash = this.configGroupHashService.computeHash(
ConfigVariablesGroup.EMAIL_SETTINGS,
);
@@ -9,23 +9,25 @@ import { AwsSesHandleErrorService } from 'src/engine/core-modules/emailing-domai
import { EmailingDomainDriver } from 'src/engine/core-modules/emailing-domain/drivers/types/emailing-domain';
import { DriverFactoryBase } from 'src/engine/core-modules/twenty-config/dynamic-factory.base';
import { ConfigVariablesGroup } from 'src/engine/core-modules/twenty-config/enums/config-variables-group.enum';
import { ConfigGroupHashService } from 'src/engine/core-modules/twenty-config/services/config-group-hash.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
@Injectable()
export class EmailingDomainDriverFactory extends DriverFactoryBase<EmailingDomainDriverInterface> {
constructor(
twentyConfigService: TwentyConfigService,
configGroupHashService: ConfigGroupHashService,
private readonly awsSesClientProvider: AwsSesClientProvider,
private readonly awsSesHandleErrorService: AwsSesHandleErrorService,
) {
super(twentyConfigService);
super(twentyConfigService, configGroupHashService);
}
protected buildConfigKey(): string {
const driver = EmailingDomainDriver.AWS_SES;
if (driver === EmailingDomainDriver.AWS_SES) {
const awsConfigHash = this.getConfigGroupHash(
const awsConfigHash = this.configGroupHashService.computeHash(
ConfigVariablesGroup.AWS_SES_SETTINGS,
);
@@ -3,15 +3,20 @@ import { Test, type TestingModule } from '@nestjs/testing';
import { StorageDriverType } from 'src/engine/core-modules/file-storage/interfaces/file-storage.interface';
import { FileStorageDriverFactory } from 'src/engine/core-modules/file-storage/file-storage-driver.factory';
import { ConfigGroupHashService } from 'src/engine/core-modules/twenty-config/services/config-group-hash.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
describe('FileStorageDriverFactory', () => {
let factory: FileStorageDriverFactory;
let twentyConfigService: TwentyConfigService;
let configGroupHashService: ConfigGroupHashService;
const mockTwentyConfigService = {
get: jest.fn(),
};
const mockConfigGroupHashService = {
computeHash: jest.fn().mockReturnValue(''),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
@@ -21,11 +26,18 @@ describe('FileStorageDriverFactory', () => {
provide: TwentyConfigService,
useValue: mockTwentyConfigService,
},
{
provide: ConfigGroupHashService,
useValue: mockConfigGroupHashService,
},
],
}).compile();
factory = module.get<FileStorageDriverFactory>(FileStorageDriverFactory);
twentyConfigService = module.get<TwentyConfigService>(TwentyConfigService);
configGroupHashService = module.get<ConfigGroupHashService>(
ConfigGroupHashService,
);
jest.clearAllMocks();
});
@@ -57,7 +69,7 @@ describe('FileStorageDriverFactory', () => {
.spyOn(twentyConfigService, 'get')
.mockReturnValue(StorageDriverType.S_3);
jest
.spyOn(factory as any, 'getConfigGroupHash')
.spyOn(configGroupHashService, 'computeHash')
.mockReturnValue('s3-hash-123');
const result = factory['buildConfigKey']();
@@ -259,7 +271,7 @@ describe('FileStorageDriverFactory', () => {
}
});
jest
.spyOn(factory as any, 'getConfigGroupHash')
.spyOn(configGroupHashService, 'computeHash')
.mockReturnValue('s3-hash-123');
const driver2 = factory.getCurrentDriver();
@@ -10,13 +10,17 @@ import { S3Driver } from 'src/engine/core-modules/file-storage/drivers/s3.driver
import { ValidatedStorageDriver } from 'src/engine/core-modules/file-storage/drivers/validated-storage.driver';
import { DriverFactoryBase } from 'src/engine/core-modules/twenty-config/dynamic-factory.base';
import { ConfigVariablesGroup } from 'src/engine/core-modules/twenty-config/enums/config-variables-group.enum';
import { ConfigGroupHashService } from 'src/engine/core-modules/twenty-config/services/config-group-hash.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { resolveAbsolutePath } from 'src/utils/resolve-absolute-path';
@Injectable()
export class FileStorageDriverFactory extends DriverFactoryBase<StorageDriver> {
constructor(twentyConfigService: TwentyConfigService) {
super(twentyConfigService);
constructor(
twentyConfigService: TwentyConfigService,
configGroupHashService: ConfigGroupHashService,
) {
super(twentyConfigService, configGroupHashService);
}
protected buildConfigKey(): string {
@@ -29,7 +33,7 @@ export class FileStorageDriverFactory extends DriverFactoryBase<StorageDriver> {
}
if (storageType === StorageDriverType.S_3) {
const storageConfigHash = this.getConfigGroupHash(
const storageConfigHash = this.configGroupHashService.computeHash(
ConfigVariablesGroup.STORAGE_CONFIG,
);
@@ -15,24 +15,26 @@ import { LogicFunctionResourceService } from 'src/engine/core-modules/logic-func
import { SdkClientArchiveService } from 'src/engine/core-modules/sdk-client/sdk-client-archive.service';
import { DriverFactoryBase } from 'src/engine/core-modules/twenty-config/dynamic-factory.base';
import { ConfigVariablesGroup } from 'src/engine/core-modules/twenty-config/enums/config-variables-group.enum';
import { ConfigGroupHashService } from 'src/engine/core-modules/twenty-config/services/config-group-hash.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
@Injectable()
export class LogicFunctionDriverFactory extends DriverFactoryBase<LogicFunctionDriver> {
constructor(
twentyConfigService: TwentyConfigService,
configGroupHashService: ConfigGroupHashService,
private readonly logicFunctionResourceService: LogicFunctionResourceService,
private readonly sdkClientArchiveService: SdkClientArchiveService,
private readonly cacheLockService: CacheLockService,
) {
super(twentyConfigService);
super(twentyConfigService, configGroupHashService);
}
protected buildConfigKey(): string {
const driverType = this.twentyConfigService.get('LOGIC_FUNCTION_TYPE');
if (driverType === LogicFunctionDriverType.LAMBDA) {
return `lambda|${this.getConfigGroupHash(ConfigVariablesGroup.LOGIC_FUNCTION_CONFIG)}`;
return `lambda|${this.configGroupHashService.computeHash(ConfigVariablesGroup.LOGIC_FUNCTION_CONFIG)}`;
}
return driverType;
@@ -1,15 +1,14 @@
import { createHash } from 'crypto';
import { ConfigVariables } from 'src/engine/core-modules/twenty-config/config-variables';
import { type ConfigVariablesGroup } from 'src/engine/core-modules/twenty-config/enums/config-variables-group.enum';
import { type ConfigGroupHashService } from 'src/engine/core-modules/twenty-config/services/config-group-hash.service';
import { type TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { TypedReflect } from 'src/utils/typed-reflect';
export abstract class DriverFactoryBase<TDriver> {
private currentDriver: TDriver | null = null;
private currentConfigKey: string | null = null;
constructor(protected readonly twentyConfigService: TwentyConfigService) {}
constructor(
protected readonly twentyConfigService: TwentyConfigService,
protected readonly configGroupHashService: ConfigGroupHashService,
) {}
getCurrentDriver(): TDriver {
let configKey: string;
@@ -43,31 +42,6 @@ export abstract class DriverFactoryBase<TDriver> {
return this.currentDriver;
}
protected getConfigGroupHash(group: ConfigVariablesGroup): string {
const groupVariables = this.getConfigVariablesByGroup(group);
const configValues = groupVariables
.map((key) => `${key}=${this.twentyConfigService.get(key)}`)
.sort()
.join('|');
return createHash('sha256')
.update(configValues)
.digest('hex')
.substring(0, 16);
}
private getConfigVariablesByGroup(
group: ConfigVariablesGroup,
): Array<keyof ConfigVariables> {
const metadata =
TypedReflect.getMetadata('config-variables', ConfigVariables) ?? {};
return Object.keys(metadata)
.filter((key) => metadata[key]?.group === group)
.map((key) => key as keyof ConfigVariables);
}
protected abstract buildConfigKey(): string;
protected abstract createDriver(): TDriver;
}
@@ -0,0 +1,40 @@
import { Injectable } from '@nestjs/common';
import { createHash } from 'crypto';
import { ConfigVariables } from 'src/engine/core-modules/twenty-config/config-variables';
import { type ConfigVariablesGroup } from 'src/engine/core-modules/twenty-config/enums/config-variables-group.enum';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { TypedReflect } from 'src/utils/typed-reflect';
@Injectable()
export class ConfigGroupHashService {
constructor(private readonly twentyConfigService: TwentyConfigService) {}
computeHash(group: ConfigVariablesGroup): string {
const groupVariables = this.getConfigVariablesByGroup(group);
const configValues = groupVariables
.map(
(key) => `${key}=${JSON.stringify(this.twentyConfigService.get(key))}`,
)
.sort()
.join('|');
return createHash('sha256')
.update(configValues)
.digest('hex')
.substring(0, 16);
}
private getConfigVariablesByGroup(
group: ConfigVariablesGroup,
): Array<keyof ConfigVariables> {
const metadata =
TypedReflect.getMetadata('config-variables', ConfigVariables) ?? {};
return Object.keys(metadata)
.filter((key) => metadata[key]?.group === group)
.map((key) => key as keyof ConfigVariables);
}
}
@@ -3,6 +3,7 @@ import { type DynamicModule, Global, Module } from '@nestjs/common';
import { ConfigVariables } from 'src/engine/core-modules/twenty-config/config-variables';
import { CONFIG_VARIABLES_INSTANCE_TOKEN } from 'src/engine/core-modules/twenty-config/constants/config-variables-instance-tokens.constants';
import { DatabaseConfigModule } from 'src/engine/core-modules/twenty-config/drivers/database-config.module';
import { ConfigGroupHashService } from 'src/engine/core-modules/twenty-config/services/config-group-hash.service';
import { ConfigurableModuleClass } from 'src/engine/core-modules/twenty-config/twenty-config.module-definition';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
@@ -22,12 +23,13 @@ export class TwentyConfigModule extends ConfigurableModuleClass {
imports,
providers: [
TwentyConfigService,
ConfigGroupHashService,
{
provide: CONFIG_VARIABLES_INSTANCE_TOKEN,
useValue: new ConfigVariables(),
},
],
exports: [TwentyConfigService],
exports: [TwentyConfigService, ConfigGroupHashService],
};
}
}
@@ -7,19 +7,23 @@ import { ExaDriver } from 'src/engine/core-modules/web-search/drivers/exa.driver
import { WebSearchDriverType } from 'src/engine/core-modules/web-search/web-search.interface';
import { DriverFactoryBase } from 'src/engine/core-modules/twenty-config/dynamic-factory.base';
import { ConfigVariablesGroup } from 'src/engine/core-modules/twenty-config/enums/config-variables-group.enum';
import { ConfigGroupHashService } from 'src/engine/core-modules/twenty-config/services/config-group-hash.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
@Injectable()
export class WebSearchDriverFactory extends DriverFactoryBase<WebSearchDriver> {
constructor(twentyConfigService: TwentyConfigService) {
super(twentyConfigService);
constructor(
twentyConfigService: TwentyConfigService,
configGroupHashService: ConfigGroupHashService,
) {
super(twentyConfigService, configGroupHashService);
}
protected buildConfigKey(): string {
const driverType = this.twentyConfigService.get('WEB_SEARCH_DRIVER');
if (driverType !== WebSearchDriverType.DISABLED) {
return `${driverType}|${this.getConfigGroupHash(ConfigVariablesGroup.LLM)}`;
return `${driverType}|${this.configGroupHashService.computeHash(ConfigVariablesGroup.LLM)}`;
}
return driverType;
@@ -1,5 +1,6 @@
import { Test, type TestingModule } from '@nestjs/testing';
import { ConfigGroupHashService } from 'src/engine/core-modules/twenty-config/services/config-group-hash.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { AiModelPreferencesService } from 'src/engine/metadata-modules/ai/ai-models/services/ai-model-preferences.service';
import { ProviderConfigService } from 'src/engine/metadata-modules/ai/ai-models/services/provider-config.service';
@@ -119,6 +120,10 @@ describe('AiModelRegistryService', () => {
provide: AiModelPreferencesService,
useValue: mockPreferencesService,
},
{
provide: ConfigGroupHashService,
useValue: { computeHash: jest.fn().mockReturnValue('') },
},
],
}).compile();
@@ -3,6 +3,8 @@ import { Injectable, Logger } from '@nestjs/common';
import { type LanguageModel } from 'ai';
import { type AiSdkPackage } from 'twenty-shared/ai';
import { ConfigVariablesGroup } from 'src/engine/core-modules/twenty-config/enums/config-variables-group.enum';
import { ConfigGroupHashService } from 'src/engine/core-modules/twenty-config/services/config-group-hash.service';
import { AiModelRole } from 'src/engine/metadata-modules/ai/ai-models/types/ai-model-role.enum';
import {
@@ -50,13 +52,29 @@ export class AiModelRegistryService {
string,
{ providerName: string; modelDef: AiProviderModelConfig }
> = new Map();
private currentConfigHash: string | null = null;
constructor(
private readonly providerConfigService: ProviderConfigService,
private readonly sdkProviderFactory: SdkProviderFactoryService,
private readonly preferencesService: AiModelPreferencesService,
) {
private readonly configGroupHashService: ConfigGroupHashService,
) {}
// The registry is rebuilt lazily whenever the LLM-group config hash changes,
// so any mutation to an LLM-tagged config variable is picked up automatically
// on the next read — no explicit refresh from callers needed.
private ensureFresh(): void {
const configHash = this.configGroupHashService.computeHash(
ConfigVariablesGroup.LLM,
);
if (configHash === this.currentConfigHash) {
return;
}
this.buildModelRegistry();
this.currentConfigHash = configHash;
}
private buildModelRegistry(): void {
@@ -146,14 +164,20 @@ export class AiModelRegistryService {
}
getModel(modelId: string): RegisteredAIModel | undefined {
this.ensureFresh();
return this.modelRegistry.get(modelId);
}
getAvailableModels(): RegisteredAIModel[] {
this.ensureFresh();
return Array.from(this.modelRegistry.values());
}
getModelConfig(modelId: string): AIModelConfig | undefined {
this.ensureFresh();
return this.modelConfigCache.get(modelId);
}
@@ -205,6 +229,8 @@ export class AiModelRegistryService {
}
getEffectiveModelConfig(modelId: string): AIModelConfig {
this.ensureFresh();
if (isAutoSelectModelId(modelId)) {
const defaultModel =
modelId === AUTO_SELECT_FAST_MODEL_ID
@@ -304,6 +330,7 @@ export class AiModelRegistryService {
providerName?: string;
name?: string;
}> {
this.ensureFresh();
const recommended = this.getRecommendedModelIds();
return Array.from(this.modelConfigCache.values()).map((modelConfig) => {
@@ -340,6 +367,8 @@ export class AiModelRegistryService {
}
private validateModelInRegistry(modelId: string): void {
this.ensureFresh();
if (!this.providerModelDefCache.has(modelId)) {
throw new AgentException(
`Cannot update model "${modelId}": not found in registry`,
@@ -356,10 +385,6 @@ export class AiModelRegistryService {
return this.providerConfigService.getCatalogProviderNames();
}
refreshRegistry(): void {
this.buildModelRegistry();
}
resolveModelForAgent(agent: { modelId: string } | null): RegisteredAIModel {
const aiModel = this.getEffectiveModelConfig(
agent?.modelId ?? AUTO_SELECT_SMART_MODEL_ID,