feat(gateway): global tier provider/model defaults with catalog-backed options

This commit is contained in:
William Valentin
2026-02-19 10:17:16 -08:00
parent 5883e046ac
commit 708683297a
7 changed files with 495 additions and 5 deletions
+96 -4
View File
@@ -695,6 +695,12 @@ function updateAssistantHealth(configData) {
const modelTier = configData?.agents?.primary_tier ?? 'default';
const delegation = configData?.agents?.delegation ?? {};
const backgroundModels = configData?.agents?.background_models ?? {};
const tiers = configData?.models ?? {};
const modelCatalog = configData?.__modelCatalog ?? [];
const providerList = modelCatalog.length > 0
? modelCatalog.map((entry) => entry.provider)
: ['anthropic', 'openai', 'gemini', 'openrouter', 'github', 'xai', 'ollama', 'llamacpp', 'bedrock', 'zhipuai', 'minimax', 'moonshot', 'synthetic'];
const modelOptionsByProvider = Object.fromEntries(modelCatalog.map((entry) => [entry.provider, entry.models ?? []]));
const checklistRows = [
{ label: 'Set briefing output channel + peer', done: Boolean(briefingOutput?.channel && briefingOutput?.peer) },
{ label: 'Enable assistant behavior profile', done: playbookLikeReady },
@@ -710,6 +716,18 @@ function updateAssistantHealth(configData) {
const tierOption = (selected) => ['fast', 'default', 'complex', 'local']
.map((tier) => `<option value="${tier}" ${selected === tier ? 'selected' : ''}>${tier}</option>`)
.join('');
const providerOption = (selected) => providerList
.map((provider) => `<option value="${provider}" ${selected === provider ? 'selected' : ''}>${provider}</option>`)
.join('');
const modelDataList = (id, provider, selected) => {
const options = modelOptionsByProvider[provider] ?? [];
return `
<input id="${id}" list="${id}-list" value="${escapeHtml(selected ?? '')}" class="bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-2 py-1.5 text-sm focus:border-blue-500 outline-none" />
<datalist id="${id}-list">
${options.map((model) => `<option value="${escapeHtml(model)}"></option>`).join('')}
</datalist>
`;
};
const taskRows = [
{ key: 'compaction', label: 'Compaction' },
{ key: 'memory_extraction', label: 'Memory extraction' },
@@ -767,6 +785,31 @@ function updateAssistantHealth(configData) {
</div>
<div class="mt-4 p-4 border border-zinc-800 rounded-lg bg-zinc-900">
<div class="text-sm font-semibold text-zinc-50 mb-3">Model Tier Defaults</div>
<div class="text-sm text-zinc-500 mb-3">Tier provider/model definitions</div>
<div class="space-y-3 mb-4">
${['default', 'fast', 'complex', 'local'].map((tier) => {
const cfg = tiers?.[tier] ?? {};
const provider = cfg.provider ?? tiers?.default?.provider ?? 'openai';
const model = cfg.model ?? '';
return `
<div class="p-3 border border-zinc-800 rounded-md bg-zinc-950/60">
<div class="text-sm text-zinc-50 mb-2">${escapeHtml(tier)} tier</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
<label class="flex flex-col gap-1">
<span class="text-xs text-zinc-500">Provider</span>
<select id="assist-tier-${tier}-provider" class="bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-2 py-1.5 text-sm focus:border-blue-500 outline-none">
${providerOption(provider)}
</select>
</label>
<label class="flex flex-col gap-1">
<span class="text-xs text-zinc-500">Model</span>
${modelDataList(`assist-tier-${tier}-model`, provider, model)}
</label>
</div>
</div>
`;
}).join('')}
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 mb-4">
<label class="flex flex-col gap-1.5">
<span class="text-sm text-zinc-400">Primary tier</span>
@@ -797,11 +840,13 @@ function updateAssistantHealth(configData) {
</label>
<label class="flex flex-col gap-1">
<span class="text-xs text-zinc-500">Provider</span>
<input id="assist-bg-${task.key}-provider" type="text" value="${escapeHtml(background?.provider ?? '')}" placeholder="openai" class="bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-2 py-1.5 text-sm focus:border-blue-500 outline-none" />
<select id="assist-bg-${task.key}-provider" class="bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-2 py-1.5 text-sm focus:border-blue-500 outline-none">
${providerOption(background?.provider ?? tiers?.default?.provider ?? 'openai')}
</select>
</label>
<label class="flex flex-col gap-1">
<span class="text-xs text-zinc-500">Model</span>
<input id="assist-bg-${task.key}-model" type="text" value="${escapeHtml(background?.model ?? '')}" placeholder="gpt-4o-mini" class="bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-2 py-1.5 text-sm focus:border-blue-500 outline-none" />
${modelDataList(`assist-bg-${task.key}-model`, background?.provider ?? tiers?.default?.provider ?? 'openai', background?.model ?? '')}
</label>
<label class="flex flex-col gap-1">
<span class="text-xs text-zinc-500">Fallback tier</span>
@@ -866,6 +911,32 @@ function updateAssistantHealth(configData) {
${renderAssistantSaveState()}
`;
const updateModelOptions = (inputId, provider) => {
const input = el.querySelector(`#${inputId}`);
const list = el.querySelector(`#${inputId}-list`);
if (!input || !list) {return;}
const options = modelOptionsByProvider[provider] ?? [];
list.innerHTML = options.map((model) => `<option value="${escapeHtml(model)}"></option>`).join('');
};
const tierRows = ['default', 'fast', 'complex', 'local'];
for (const tier of tierRows) {
const providerSelect = el.querySelector(`#assist-tier-${tier}-provider`);
if (!providerSelect) {continue;}
providerSelect.addEventListener('change', () => {
updateModelOptions(`assist-tier-${tier}-model`, providerSelect.value);
});
}
const taskRowsForModels = ['compaction', 'memory_extraction', 'classification', 'tool_summarisation', 'complex_reasoning'];
for (const task of taskRowsForModels) {
const providerSelect = el.querySelector(`#assist-bg-${task}-provider`);
if (!providerSelect) {continue;}
providerSelect.addEventListener('change', () => {
updateModelOptions(`assist-bg-${task}-model`, providerSelect.value);
});
}
const statusEl = el.querySelector('#ops-assistant-status');
const buttons = el.querySelectorAll('.assistant-action-btn');
buttons.forEach((button) => {
@@ -929,6 +1000,20 @@ function updateAssistantHealth(configData) {
patches = {
'agents.primary_tier': (el.querySelector('#assist-primary-tier')?.value ?? 'default'),
};
const tiers = ['default', 'fast', 'complex', 'local'];
for (const tier of tiers) {
const provider = (el.querySelector(`#assist-tier-${tier}-provider`)?.value ?? '').trim();
const model = (el.querySelector(`#assist-tier-${tier}-model`)?.value ?? '').trim();
if (!provider || (!model && tier !== 'default')) {
continue;
}
if (provider) {
patches[`models.${tier}.provider`] = provider;
}
if (model) {
patches[`models.${tier}.model`] = model;
}
}
for (const task of tasks) {
const delegationTier = el.querySelector(`#assist-delegation-${task}`)?.value ?? 'fast';
const enabled = Boolean(el.querySelector(`#assist-bg-${task}-enabled`)?.checked);
@@ -1042,22 +1127,29 @@ async function fetchFast(client) {
}
async function fetchSlow(client) {
const [health, services, sessionAnalytics, contextUsage, config] = await Promise.allSettled([
const [health, services, sessionAnalytics, contextUsage, config, modelCatalog] = await Promise.allSettled([
client.call('system.health'),
client.call('system.services'),
client.call('system.sessionAnalytics', { days: 14, topLimit: 5 }),
client.call('system.contextUsage'),
client.call('config.get'),
client.call('system.modelCatalog'),
]);
const unwrap = (result) => (result.status === 'fulfilled' ? result.value : null);
const configValue = unwrap(config);
const modelCatalogValue = unwrap(modelCatalog);
if (configValue && typeof configValue === 'object') {
configValue.__modelCatalog = Array.isArray(modelCatalogValue?.providers) ? modelCatalogValue.providers : [];
}
return {
health: unwrap(health),
services: unwrap(services),
sessionAnalytics: unwrap(sessionAnalytics),
contextUsage: unwrap(contextUsage),
config: unwrap(config),
config: configValue,
};
}