chore: retire zen-backed summarization

Disable the active Zen summarization flow because the unauthenticated/free Zen provider is no longer available and now returns usage-limit errors for this feature.

Keep /api/text/summarize as an API-compatible stub that returns local sanitized or distilled fallback text with summarized=false, rather than attempting external model calls.

Remove notification and voice playback summary behavior from runtime paths. Notification {last_message} now always uses normalized truncated text, and TTS playback ignores historical summarize request fields.

Hide the notification summary settings and voice summarize-before-playback controls while preserving legacy persisted settings for compatibility. Also disable Zen model startup validation and make Zen model list routes return empty results.

Update module documentation and tests to describe the retired provider behavior and the remaining compatibility stubs.
This commit is contained in:
Bohdan Triapitsyn
2026-05-19 02:06:52 +03:00
parent a57b02a308
commit 174fa4e96d
27 changed files with 137 additions and 1136 deletions
@@ -3,20 +3,15 @@ import { summarizeText as summarizeSharedText } from '../text/summarization.js';
export const createNotificationTemplateRuntime = (deps) => {
const {
readSettingsFromDisk,
persistSettings,
buildOpenCodeUrl,
getOpenCodeAuthHeaders,
resolveGitBinaryForSpawn,
} = deps;
const NOTIFICATION_BODY_MAX_CHARS = 1000;
const ZEN_DEFAULT_MODEL = 'gpt-5-nano';
const ZEN_MODELS_CACHE_TTL = 5 * 60 * 1000;
const SESSION_INFO_CACHE_TTL_MS = 60 * 1000;
let validatedZenFallback = null;
let cachedZenModels = null;
let cachedZenModelsTimestamp = 0;
const cachedZenModels = { models: [] };
const sessionTitleCache = new Map();
const sessionInfoCache = new Map();
@@ -62,116 +57,18 @@ export const createNotificationTemplateRuntime = (deps) => {
return true;
};
const fetchFreeZenModels = async () => {
const now = Date.now();
if (cachedZenModels && now - cachedZenModelsTimestamp < ZEN_MODELS_CACHE_TTL) {
return cachedZenModels.models;
}
const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
const timeout = controller ? setTimeout(() => controller.abort(), 8000) : null;
try {
const [zenResponse, metadataResponse] = await Promise.all([
fetch('https://opencode.ai/zen/v1/models', {
signal: controller?.signal,
headers: { Accept: 'application/json' },
}),
fetch('https://models.dev/api.json', {
signal: controller?.signal,
headers: { Accept: 'application/json' },
}),
]);
if (!zenResponse.ok) {
throw new Error(`zen/v1/models responded with status ${zenResponse.status}`);
}
if (!metadataResponse.ok) {
throw new Error(`models.dev responded with status ${metadataResponse.status}`);
}
const data = await zenResponse.json();
const metadata = await metadataResponse.json();
const metadataModels = metadata?.opencode?.models && typeof metadata.opencode.models === 'object'
? metadata.opencode.models
: {};
const allModels = Array.isArray(data?.data) ? data.data : [];
const freeModels = allModels
.filter((model) => {
const id = typeof model?.id === 'string' ? model.id.trim() : '';
const cost = id ? metadataModels[id]?.cost : null;
return id && cost?.input === 0 && cost?.output === 0;
})
.map((model) => ({ id: model.id.trim(), owned_by: model.owned_by }));
cachedZenModels = { models: freeModels };
cachedZenModelsTimestamp = Date.now();
return freeModels;
} finally {
if (timeout) clearTimeout(timeout);
}
};
const fetchFreeZenModels = async () => [];
const resolveZenModel = async (override) => {
const overrideModel = typeof override === 'string' ? override.trim() : '';
let settingsModel = '';
try {
const settings = await readSettingsFromDisk();
if (typeof settings?.zenModel === 'string' && settings.zenModel.trim().length > 0) {
settingsModel = settings.zenModel.trim();
}
} catch {
}
const candidate = overrideModel || settingsModel;
try {
const models = await fetchFreeZenModels();
const modelIds = models.map((model) => model.id);
if (candidate && modelIds.includes(candidate)) {
return candidate;
}
if (modelIds.includes(ZEN_DEFAULT_MODEL)) {
return ZEN_DEFAULT_MODEL;
}
if (modelIds.length > 0) {
return modelIds[0];
}
} catch {
if (candidate) {
return candidate;
}
}
return validatedZenFallback || ZEN_DEFAULT_MODEL;
if (overrideModel) return overrideModel;
const settings = await readSettingsFromDisk().catch(() => ({}));
return typeof settings?.zenModel === 'string' && settings.zenModel.trim().length > 0
? settings.zenModel.trim()
: '';
};
const validateZenModelAtStartup = async () => {
try {
const freeModels = await fetchFreeZenModels();
const freeModelIds = freeModels.map((model) => model.id);
if (freeModelIds.length > 0) {
validatedZenFallback = freeModelIds[0];
const settings = await readSettingsFromDisk();
const storedModel = typeof settings?.zenModel === 'string' ? settings.zenModel.trim() : '';
if (!storedModel || !freeModelIds.includes(storedModel)) {
const fallback = freeModelIds[0];
console.log(
storedModel
? `[zen] Stored model "${storedModel}" not found in free models, falling back to "${fallback}"`
: `[zen] No model configured, setting default to "${fallback}"`
);
await persistSettings({ zenModel: fallback });
} else {
console.log(`[zen] Stored model "${storedModel}" verified as available`);
}
} else {
console.warn('[zen] No free models returned from API, skipping validation');
}
} catch (error) {
console.warn('[zen] Startup model validation failed (non-blocking):', error?.message || error);
}
};
const validateZenModelAtStartup = async () => {};
const summarizeText = async (text, targetLength, zenModel) => {
if (!text || typeof text !== 'string' || text.trim().length === 0) return text;
@@ -179,7 +76,7 @@ export const createNotificationTemplateRuntime = (deps) => {
text,
threshold: 0,
maxLength: targetLength,
zenModel: zenModel || ZEN_DEFAULT_MODEL,
zenModel,
mode: 'notification',
});
return typeof result?.summary === 'string' && result.summary.trim().length > 0