forked from bai/curriculum-project-hub
feat(hub): derive admin model picker from org provider connection via OpenRouter API
The admin role model picker was hardcoded to the env-default model registry (createDefaultModelRegistry), which only ever returned a single Sonnet model. Roles could not select any other model regardless of what the org's provider connection supported. Replace the env-only model list with a ProviderModelCatalog that: - Resolves the org's ACTIVE provider connection credential (BYOK or platform-managed, encrypted via ADR-0024 envelope) - Calls OpenRouter GET /v1/models?supported_parameters=tools to list tool-capable models available to that org - Caches results in-memory with a 5-minute TTL per organization - Falls back to the env-default registry when no ACTIVE provider exists The runtime modelRegistry no longer validates role.defaultModel against the env model list — the admin already validated by selection from the provider catalog. The env list remains as the fallback for roles with null defaultModel. The admin roles page loads models independently (non-blocking) so roles remain editable even if the provider API is slow or unreachable.
This commit is contained in:
@@ -182,12 +182,16 @@ export class DatabaseRuntimeSettings implements RuntimeSettings {
|
||||
throw new Error(`organization ${project.organization.id} must have exactly one active default Agent role`);
|
||||
}
|
||||
|
||||
// The model list is the env-based fallback used when a role has no
|
||||
// explicit defaultModel. Role defaultModels are chosen from the provider
|
||||
// model catalog (admin surface) and may not be in the env list — we don't
|
||||
// validate against it at runtime; the admin already validated by selection.
|
||||
const defaults = await this.envSettings.modelRegistry(scope);
|
||||
const enabledModels = new Set(defaults.listModels().map((model) => model.id));
|
||||
const fallbackModels = defaults.listModels();
|
||||
const roles: RoleEntry[] = project.organization.agentRoles.map((role) => ({
|
||||
id: role.roleId,
|
||||
label: role.label,
|
||||
defaultModel: validateRoleModel(role.roleId, role.defaultModel, enabledModels),
|
||||
defaultModel: role.defaultModel ?? undefined,
|
||||
...(role.systemPrompt !== null ? { systemPrompt: role.systemPrompt } : {}),
|
||||
...(role.tools !== null ? { tools: roleToolsFromJson(role.roleId, role.tools) } : {}),
|
||||
skills: role.skillBindings.map((binding): RoleSkillEntry => {
|
||||
@@ -210,7 +214,7 @@ export class DatabaseRuntimeSettings implements RuntimeSettings {
|
||||
};
|
||||
}),
|
||||
}));
|
||||
return new InMemoryModelRegistry(defaults.listModels(), roles);
|
||||
return new InMemoryModelRegistry(fallbackModels, roles);
|
||||
}
|
||||
|
||||
runPolicy(input: RunPolicyInput): Promise<RunPolicy> {
|
||||
@@ -225,15 +229,6 @@ function roleToolsFromJson(roleId: string, value: Prisma.JsonValue): readonly st
|
||||
return value as string[];
|
||||
}
|
||||
|
||||
function validateRoleModel(
|
||||
roleId: string,
|
||||
model: string | null,
|
||||
enabledModels: ReadonlySet<string>,
|
||||
): string | undefined {
|
||||
if (model === null) return undefined;
|
||||
if (!enabledModels.has(model)) throw new Error(`role ${roleId} selects unavailable model ${model}`);
|
||||
return model;
|
||||
}
|
||||
|
||||
async function loadActiveProviderSecret(
|
||||
tx: Prisma.TransactionClient,
|
||||
|
||||
Reference in New Issue
Block a user