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:
@@ -1,10 +1,12 @@
|
||||
/**
|
||||
* Org-admin Agent configuration routes (ADR-0017 / ADR-0018).
|
||||
*
|
||||
* Surface for browsing and editing Organization-scoped Agent roles and skills.
|
||||
* Roles are org-owned data; the default-model picker is constrained to the
|
||||
* env-default model registry (ADR-0017: there is no org-scoped model list —
|
||||
* `OrganizationAgentRole.defaultModel` selects from the platform-enabled set).
|
||||
* Surface for browsing and editing Organization-scoped Agent roles, the skills
|
||||
* bound to them, and the model picker. The model list is **derived** from the
|
||||
* org's ACTIVE provider connection via OpenRouter's /v1/models API — there is
|
||||
* no separate model table to maintain. When no ACTIVE provider exists, the
|
||||
* route falls back to the env-default model registry so the admin surface
|
||||
* remains usable.
|
||||
*
|
||||
* Skill management (create, read, edit, disable) is performed in-band through
|
||||
* the deep module `OrganizationAgentConfiguration`, which writes to the same
|
||||
@@ -17,7 +19,9 @@
|
||||
import type { PrismaClient } from "@prisma/client";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { OrganizationAgentConfiguration } from "../../agent/configuration.js";
|
||||
import { ProviderModelCatalog } from "../../connections/providerModelCatalog.js";
|
||||
import { createDefaultModelRegistry } from "../../settings/runtime.js";
|
||||
import type { LocalSecretEnvelope } from "../../security/secretEnvelope.js";
|
||||
import { requireOrgRole, type GuardDeps } from "../auth/guards.js";
|
||||
import { handleRouteError } from "../errors.js";
|
||||
|
||||
@@ -25,6 +29,7 @@ export interface AgentConfigRouteConfig {
|
||||
readonly prisma: PrismaClient;
|
||||
readonly sessionSecret: string;
|
||||
readonly skillStoreRoot: string;
|
||||
readonly secretEnvelope: LocalSecretEnvelope;
|
||||
}
|
||||
|
||||
export async function registerAgentConfigRoutes(
|
||||
@@ -33,6 +38,7 @@ export async function registerAgentConfigRoutes(
|
||||
): Promise<void> {
|
||||
const guardDeps: GuardDeps = { prisma: config.prisma, sessionSecret: config.sessionSecret };
|
||||
const agentConfig = new OrganizationAgentConfiguration(config.prisma, config.skillStoreRoot);
|
||||
const modelCatalog = new ProviderModelCatalog(config.prisma, config.secretEnvelope);
|
||||
|
||||
app.get("/api/org/:orgSlug/agent-roles", async (request, reply) => {
|
||||
try {
|
||||
@@ -226,8 +232,13 @@ export async function registerAgentConfigRoutes(
|
||||
const { orgSlug } = request.params as { orgSlug: string };
|
||||
const auth = await requireOrgRole(request, reply, guardDeps, { orgSlug });
|
||||
if (auth === null) return;
|
||||
const registry = createDefaultModelRegistry(process.env);
|
||||
return { models: registry.listModels() };
|
||||
const providerModels = await modelCatalog.listModels(auth.organization.id);
|
||||
// Fall back to env-default registry when the org has no ACTIVE provider
|
||||
// (or the provider API is unreachable on first load).
|
||||
const models = providerModels.length > 0
|
||||
? providerModels
|
||||
: createDefaultModelRegistry(process.env).listModels();
|
||||
return { models };
|
||||
} catch (err) {
|
||||
return handleRouteError(reply, err);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user