fix: use valid Zen summaries for notes

This commit is contained in:
Bohdan Triapitsyn
2026-04-30 13:07:37 +03:00
parent fa71954ef1
commit bbd83d60c6
16 changed files with 430 additions and 62 deletions
+54 -8
View File
@@ -131,6 +131,42 @@ function extractZenOutputText(data) {
return text || null;
}
function extractZenChatCompletionText(data) {
if (!data || typeof data !== 'object') return null;
const choices = data.choices;
if (!Array.isArray(choices)) return null;
const choice = choices.find((item) => item && typeof item === 'object');
const content = choice?.message?.content;
if (typeof content === 'string') {
const text = content.trim();
return text || null;
}
if (!Array.isArray(content)) return null;
const text = content
.map((item) => {
if (typeof item === 'string') return item;
if (item && typeof item === 'object' && typeof item.text === 'string') return item.text;
return '';
})
.join('')
.trim();
return text || null;
}
function getZenCompletionEndpoint(model) {
if (typeof model !== 'string') return 'responses';
if (
model.startsWith('gpt-')
|| model.startsWith('claude-')
|| model.startsWith('gemini-')
) {
return 'responses';
}
return 'chat/completions';
}
function distillNoteFallback(text, maxLength) {
const sanitized = sanitizeForNote(text);
if (!sanitized) return '';
@@ -176,15 +212,23 @@ export async function summarizeText({ text, threshold = 200, maxLength = 500, ze
try {
const prompt = buildSummarizationPrompt(maxLength, mode);
const response = await fetch('https://opencode.ai/zen/v1/responses', {
const model = zenModel || 'gpt-5-nano';
const endpoint = getZenCompletionEndpoint(model);
const response = await fetch(`https://opencode.ai/zen/v1/${endpoint}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: zenModel || 'gpt-5-nano',
input: [{ role: 'user', content: `${prompt}\n\nText to summarize:\n${text}` }],
stream: false,
reasoning: { effort: 'low' },
}),
body: JSON.stringify(endpoint === 'responses'
? {
model,
input: [{ role: 'user', content: `${prompt}\n\nText to summarize:\n${text}` }],
stream: false,
reasoning: { effort: 'low' },
}
: {
model,
messages: [{ role: 'user', content: `${prompt}\n\nText to summarize:\n${text}` }],
stream: false,
}),
signal: controller.signal,
});
@@ -199,7 +243,9 @@ export async function summarizeText({ text, threshold = 200, maxLength = 500, ze
}
const data = await response.json();
const summary = extractZenOutputText(data);
const summary = endpoint === 'responses'
? extractZenOutputText(data)
: extractZenChatCompletionText(data);
if (summary) {
const sanitized = sanitizeByMode(summary, mode);
@@ -0,0 +1,64 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import { summarizeText } from './summarization.js';
describe('text summarization zen requests', () => {
afterEach(() => {
vi.unstubAllGlobals();
});
it('uses responses endpoint for gpt models', async () => {
const fetchMock = vi.fn(async () => ({
ok: true,
json: async () => ({
output: [{
type: 'message',
content: [{ type: 'output_text', text: 'Short summary' }],
}],
}),
}));
vi.stubGlobal('fetch', fetchMock);
const result = await summarizeText({
text: 'Long text '.repeat(30),
threshold: 0,
maxLength: 100,
zenModel: 'gpt-5-nano',
mode: 'notification',
});
expect(fetchMock).toHaveBeenCalledWith(
'https://opencode.ai/zen/v1/responses',
expect.objectContaining({
body: expect.stringContaining('"input"'),
}),
);
expect(result.summary).toBe('Short summary');
});
it('uses chat completions endpoint for openai-compatible zen models', async () => {
const fetchMock = vi.fn(async () => ({
ok: true,
json: async () => ({
choices: [{ message: { content: 'Chat summary' } }],
}),
}));
vi.stubGlobal('fetch', fetchMock);
const result = await summarizeText({
text: 'Long text '.repeat(30),
threshold: 0,
maxLength: 100,
zenModel: 'big-pickle',
mode: 'notification',
});
expect(fetchMock).toHaveBeenCalledWith(
'https://opencode.ai/zen/v1/chat/completions',
expect.objectContaining({
body: expect.stringContaining('"messages"'),
}),
);
expect(result.summary).toBe('Chat summary');
});
});