Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
c9bde7a
fix: add forwarded IP headers to searxng requests
stablegenius49 Mar 11, 2026
596fa37
fix: ignore blank and invalid SEARXNG env values
Mar 13, 2026
7e74eb6
fix: prevent repeated text on reconnect by using block snapshots
VibhorGautam Mar 8, 2026
3b6d03c
feat(providers): add Venice.ai as a model provider
joshua-mo-143 Mar 3, 2026
0d06d16
feat(lmstudio): add generateObject override for OpenAI-compatible APIs
teknomage8 Jan 4, 2026
bb8aecc
Fix spelling in api.ts
northern-64bit Mar 16, 2026
0403d0b
Fix spelling in index.ts
northern-64bit Mar 16, 2026
6265b17
Spelling fixes in webSearch.ts
northern-64bit Mar 16, 2026
ba74f87
fix: strip markdown code fences from LLM JSON responses
willtwilson Mar 8, 2026
1076f8e
fix: relax Groq structured output plumbing
stablegenius49 Mar 11, 2026
e5b7d95
fix: guard against undefined queries/urls in research actions
VibhorGautam Mar 8, 2026
e2fe7c9
fix: block local/private URL scraping
stablegenius49 Mar 11, 2026
d8595a2
fix: cap raw HTML before markdown conversion
VibhorGautam Mar 8, 2026
1a1cecc
fix: limit scraped content size to prevent excessive token usage
VibhorGautam Mar 8, 2026
6e4da1c
fix(search): cap writer context size
Mar 11, 2026
30dd80d
fix: surface search pipeline failures instead of hanging (#1054)
Eternalhazed Mar 18, 2026
43b79ae
fix(stream): keep chat SSE alive during long LM Studio generations
stablegenius49 Mar 11, 2026
9c2788a
fix: handle missing opening think tag for deepseek-r1-671b (#1034)
Eternalhazed Mar 18, 2026
42ed64f
fix: improve error handling for streaming tool calls and search
NicolasArnouts Mar 9, 2026
ed66af5
fix(providers): handle empty responses in generateObject
NicolasArnouts Mar 9, 2026
8fab50b
fix(openai): use standard chat completions API for OpenRouter compati…
NicolasArnouts Mar 9, 2026
e688410
fix: throw on SearXNG errors instead of swallowing, guard res.json()
VibhorGautam Mar 8, 2026
b088120
fix: improve robustness across model providers and search pipeline
VibhorGautam Mar 8, 2026
1032f6a
fix: fix spelling errors and trailing comma in classifier prompt (#1065)
Eternalhazed Mar 18, 2026
0209d0c
fix: resolve type error in OpenAI tool call arguments parsing
Eternalhazed Mar 18, 2026
7780960
chore: add .claude/settings.local.json to gitignore
Eternalhazed Mar 18, 2026
57f0ab7
fix: address 18 code review findings from CodeRabbit + Copilot
Eternalhazed Mar 19, 2026
c9db1d0
fix: address round 2 code review findings from CodeRabbit
Eternalhazed Mar 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ dist/

# Config files
config.toml
.claude/settings.local.json

# Log files
logs/
Expand Down
110 changes: 66 additions & 44 deletions src/app/api/chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ const ensureChatExists = async (input: {
};

export const POST = async (req: Request) => {
let safeClose: (() => void) | undefined;
let disconnect: (() => void) | undefined;

try {
const reqBody = (await req.json()) as Body;

Expand Down Expand Up @@ -155,57 +158,66 @@ export const POST = async (req: Request) => {
const responseStream = new TransformStream();
const writer = responseStream.writable.getWriter();
const encoder = new TextEncoder();
const keepAliveMs = 15_000;
let streamClosed = false;
let keepAliveInterval: ReturnType<typeof setInterval> | undefined;

const safeWrite = (payload: Record<string, unknown>) => {
if (streamClosed) return;

writer.write(encoder.encode(JSON.stringify(payload) + '\n')).catch((error) => {
console.warn('Failed to write chat stream payload:', error);
streamClosed = true;
if (keepAliveInterval) {
clearInterval(keepAliveInterval);
}
});
};

safeClose = () => {
if (streamClosed) return;

streamClosed = true;

const disconnect = session.subscribe((event: string, data: any) => {
if (keepAliveInterval) {
clearInterval(keepAliveInterval);
}

writer.close().catch((error) => {
console.warn('Failed to close chat stream:', error);
});
};

disconnect = session.subscribe((event: string, data: any) => {
if (event === 'data') {
if (data.type === 'block') {
writer.write(
encoder.encode(
JSON.stringify({
type: 'block',
block: data.block,
}) + '\n',
),
);
safeWrite({
type: 'block',
block: data.block,
});
} else if (data.type === 'updateBlock') {
writer.write(
encoder.encode(
JSON.stringify({
type: 'updateBlock',
blockId: data.blockId,
patch: data.patch,
}) + '\n',
),
);
safeWrite({
type: 'updateBlock',
blockId: data.blockId,
patch: data.patch,
});
} else if (data.type === 'researchComplete') {
writer.write(
encoder.encode(
JSON.stringify({
type: 'researchComplete',
}) + '\n',
),
);
safeWrite({
type: 'researchComplete',
});
}
} else if (event === 'end') {
writer.write(
encoder.encode(
JSON.stringify({
type: 'messageEnd',
}) + '\n',
),
);
writer.close();
safeWrite({
type: 'messageEnd',
});
safeClose?.();
session.removeAllListeners();
} else if (event === 'error') {
writer.write(
encoder.encode(
JSON.stringify({
type: 'error',
data: data.data,
}) + '\n',
),
);
writer.close();
safeWrite({
type: 'error',
data: data.data,
});
safeClose?.();
session.removeAllListeners();
}
});
Expand Down Expand Up @@ -233,10 +245,18 @@ export const POST = async (req: Request) => {
});

req.signal.addEventListener('abort', () => {
disconnect();
writer.close();
disconnect?.();
safeClose?.();
});

// Start keepalives only after setup succeeds
if (!streamClosed) {
keepAliveInterval = setInterval(() => {
safeWrite({ type: 'keepAlive' });
}, keepAliveMs);
safeWrite({ type: 'keepAlive' });
}

return new Response(responseStream.readable, {
headers: {
'Content-Type': 'text/event-stream',
Expand All @@ -245,6 +265,8 @@ export const POST = async (req: Request) => {
},
});
} catch (err) {
disconnect?.();
safeClose?.();
console.error('An error occurred while processing chat request:', err);
return Response.json(
{ message: 'An error occurred while processing chat request' },
Expand Down
122 changes: 76 additions & 46 deletions src/app/api/reconnect/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ export const POST = async (
req: Request,
{ params }: { params: Promise<{ id: string }> },
) => {
let safeClose: (() => void) | undefined;
let disconnect: (() => void) | undefined;

try {
const { id } = await params;

Expand All @@ -16,66 +19,91 @@ export const POST = async (
const responseStream = new TransformStream();
const writer = responseStream.writable.getWriter();
const encoder = new TextEncoder();
const keepAliveMs = 15_000;
let streamClosed = false;
let keepAliveInterval: ReturnType<typeof setInterval> | undefined;

const safeWrite = (payload: Record<string, unknown>) => {
if (streamClosed) return;

writer.write(encoder.encode(JSON.stringify(payload) + '\n')).catch((error) => {
console.warn('Failed to write reconnect stream payload:', error);
streamClosed = true;
if (keepAliveInterval) {
clearInterval(keepAliveInterval);
}
});
};

const disconnect = session.subscribe((event, data) => {
safeClose = () => {
if (streamClosed) return;

streamClosed = true;

if (keepAliveInterval) {
clearInterval(keepAliveInterval);
}

writer.close().catch((error) => {
console.warn('Failed to close reconnect stream:', error);
});
};

disconnect = session.subscribe((event, data) => {
if (event === 'data') {
if (data.type === 'block') {
writer.write(
encoder.encode(
JSON.stringify({
type: 'block',
block: data.block,
}) + '\n',
),
);
safeWrite({
type: 'block',
block: data.block,
});
} else if (data.type === 'updateBlock') {
writer.write(
encoder.encode(
JSON.stringify({
type: 'updateBlock',
blockId: data.blockId,
patch: data.patch,
}) + '\n',
),
);
safeWrite({
type: 'updateBlock',
blockId: data.blockId,
patch: data.patch,
});
} else if (data.type === 'researchComplete') {
writer.write(
encoder.encode(
JSON.stringify({
type: 'researchComplete',
}) + '\n',
),
);
safeWrite({
type: 'researchComplete',
});
}
} else if (event === 'end') {
writer.write(
encoder.encode(
JSON.stringify({
type: 'messageEnd',
}) + '\n',
),
);
writer.close();
disconnect();
safeWrite({
type: 'messageEnd',
});
safeClose?.();
if (disconnect) {
disconnect();
} else {
queueMicrotask(() => disconnect?.());
}
} else if (event === 'error') {
writer.write(
encoder.encode(
JSON.stringify({
type: 'error',
data: data.data,
}) + '\n',
),
);
writer.close();
disconnect();
safeWrite({
type: 'error',
data: data.data,
});
safeClose?.();
if (disconnect) {
disconnect();
} else {
queueMicrotask(() => disconnect?.());
}
}
});

req.signal.addEventListener('abort', () => {
disconnect();
writer.close();
disconnect?.();
safeClose?.();
});

// Start keepalives only after setup succeeds
if (!streamClosed) {
keepAliveInterval = setInterval(() => {
safeWrite({ type: 'keepAlive' });
}, keepAliveMs);
safeWrite({ type: 'keepAlive' });
}

return new Response(responseStream.readable, {
headers: {
'Content-Type': 'text/event-stream',
Expand All @@ -84,6 +112,8 @@ export const POST = async (
},
});
} catch (err) {
disconnect?.();
safeClose?.();
console.error('Error in reconnecting to session stream: ', err);
return Response.json(
{ message: 'An error has occurred.' },
Expand Down
20 changes: 16 additions & 4 deletions src/lib/agents/media/video.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,19 +43,31 @@ const searchVideos = async (
schema: schema,
});

const searchRes = await searchSearxng(res.query, {
engines: ['youtube'],
});
let searchRes;
try {
searchRes = await searchSearxng(res.query, {
engines: ['youtube'],
});
} catch (error) {
console.error('Video search failed:', error instanceof Error ? error.message : error);
return [];
}

const videos: VideoSearchResult[] = [];

searchRes.results.forEach((result) => {
if (result.thumbnail && result.url && result.title && result.iframe_src) {
// Normalize embed URL - some SearXNG instances return
// youtube-nocookie.com which doesn't resolve on all networks
const embedUrl = result.iframe_src.replace(
'www.youtube-nocookie.com/embed',
'www.youtube.com/embed',
);
videos.push({
img_src: result.thumbnail,
url: result.url,
title: result.title,
iframe_src: result.iframe_src,
iframe_src: embedUrl,
});
}
});
Expand Down
Loading