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
+80
View File
@@ -1,10 +1,13 @@
import type { GatewayRequest, OutboundMessage } from '../protocol.js';
import { makeResponse, makeError, ErrorCode } from '../protocol.js';
import { MODEL_PROVIDERS, type Config, type ModelProvider } from '../../config/index.js';
import type { ModelRouter, ModelTier } from '../../models/router.js';
import { createClientFromConfig } from '../../daemon/models.js';
export interface ConfigHandlerDeps {
config: Config;
persistConfig?: (nextConfig: Config) => Promise<void> | void;
modelRouter?: ModelRouter;
}
/**
@@ -313,6 +316,52 @@ const PATCHABLE_KEYS: Record<string, (config: Config, value: unknown) => boolean
config.agents.background_models.complex_reasoning.fallback_tier = value;
return true;
},
'models.default.provider': (config, value) => {
if (!MODEL_PROVIDERS.includes(String(value) as ModelProvider)) {return false;}
config.models.default.provider = value as ModelProvider;
return true;
},
'models.default.model': (config, value) => {
if (typeof value !== 'string' || value.trim().length === 0) {return false;}
config.models.default.model = value.trim();
return true;
},
'models.fast.provider': (config, value) => {
if (!MODEL_PROVIDERS.includes(String(value) as ModelProvider)) {return false;}
config.models.fast ??= { ...config.models.default };
config.models.fast.provider = value as ModelProvider;
return true;
},
'models.fast.model': (config, value) => {
if (typeof value !== 'string' || value.trim().length === 0) {return false;}
config.models.fast ??= { ...config.models.default };
config.models.fast.model = value.trim();
return true;
},
'models.complex.provider': (config, value) => {
if (!MODEL_PROVIDERS.includes(String(value) as ModelProvider)) {return false;}
config.models.complex ??= { ...config.models.default };
config.models.complex.provider = value as ModelProvider;
return true;
},
'models.complex.model': (config, value) => {
if (typeof value !== 'string' || value.trim().length === 0) {return false;}
config.models.complex ??= { ...config.models.default };
config.models.complex.model = value.trim();
return true;
},
'models.local.provider': (config, value) => {
if (!MODEL_PROVIDERS.includes(String(value) as ModelProvider)) {return false;}
config.models.local ??= { ...config.models.default };
config.models.local.provider = value as ModelProvider;
return true;
},
'models.local.model': (config, value) => {
if (typeof value !== 'string' || value.trim().length === 0) {return false;}
config.models.local ??= { ...config.models.default };
config.models.local.model = value.trim();
return true;
},
'automation.delivery_mode': (config, value) => {
if (value !== 'shared_session' && value !== 'isolated_job' && value !== 'announce') {return false;}
config.automation ??= {} as Config['automation'];
@@ -406,6 +455,36 @@ const PATCHABLE_KEYS: Record<string, (config: Config, value: unknown) => boolean
};
export function createConfigHandlers(deps: ConfigHandlerDeps) {
const syncModelRouterFromConfig = (nextConfig: Config): void => {
const modelRouter = deps.modelRouter;
if (!modelRouter) {
return;
}
const updateTier = (tier: ModelTier, cfg: Config['models']['default'] | undefined) => {
if (!cfg) {
return;
}
try {
const client = createClientFromConfig(cfg);
modelRouter.setClient(tier, client, `${cfg.provider}/${cfg.model}`);
modelRouter.setTierStrict(tier, false);
} catch (error) {
// Keep config.patch successful even when runtime credentials are unavailable for a selected provider.
console.warn(
`[gateway.config] Skipping runtime model router update for tier "${tier}": ${
error instanceof Error ? error.message : String(error)
}`,
);
}
};
updateTier('default', nextConfig.models.default);
updateTier('fast', nextConfig.models.fast);
updateTier('complex', nextConfig.models.complex);
updateTier('local', nextConfig.models.local);
};
return {
'config.get': async (request: GatewayRequest): Promise<OutboundMessage> => {
return makeResponse(request.id, redactConfig(deps.config));
@@ -457,6 +536,7 @@ export function createConfigHandlers(deps: ConfigHandlerDeps) {
delete (deps.config as Record<string, unknown>)[key];
}
Object.assign(deps.config as Record<string, unknown>, draft as Record<string, unknown>);
syncModelRouterFromConfig(draft);
return makeResponse(request.id, { applied, rejected, persisted: Boolean(deps.persistConfig) });
},