Skip to content

Scenario 1: Provider compatibility, search hardening, Venice.ai#1

Merged
Eternalhazed merged 28 commits intomasterfrom
scenario1-batch-ab-venice
Mar 19, 2026
Merged

Scenario 1: Provider compatibility, search hardening, Venice.ai#1
Eternalhazed merged 28 commits intomasterfrom
scenario1-batch-ab-venice

Conversation

@Eternalhazed
Copy link
Owner

@Eternalhazed Eternalhazed commented Mar 18, 2026

Summary

Consolidated batch of 18 upstream PRs (from ItzCrazyKns/Vane) targeting stability and provider compatibility. All conflicts resolved, build verified clean.

Batch A — Provider Compatibility

Batch B — Search Pipeline Hardening

Venice.ai Provider

Stats

  • 28 files changed, +1,242 / -407 lines
  • Build verified clean (npm run build passes)
  • All merge conflicts from upstream PRs resolved manually

Test plan

  • Verify OpenRouter models work (Grok, GPT-4o, Claude via OpenRouter)
  • Verify Ollama with non-default context window models (deepseek-r1, llama3)
  • Verify Groq free tier search works
  • Verify LM Studio/vLLM suggestions and search
  • Verify SearXNG search returns results (no bot detection blocks)
  • Verify search errors show error message instead of infinite spinner
  • Verify Venice.ai provider appears in settings and lists models
  • Verify DeepSeek-r1 thinking content stays in ThinkBox
  • Test scrape tool cannot access localhost/private IPs

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added Venice model provider and LMStudio object-generation support.
  • Improvements

    • More reliable streaming with periodic keep-alives and safer shutdown on disconnects/errors.
    • Hardened search/research flows with safer URL scraping, robust error handling, and controlled result truncation.
    • Safer JSON handling and token-aware text truncation utilities.
    • YouTube embed URL normalization and improved handling of extended “think” response tags.
  • Bug Fixes

    • Various crash-prevention and defensive input checks across search and researcher actions.

stablegenius49 and others added 26 commits March 18, 2026 15:13
When a client reconnects to an in-progress session, the subscribe()
method was replaying every historical event — every block creation and
every incremental updateBlock patch. This caused reconnecting clients
to visually rebuild content from scratch, producing duplicated text.

Changed subscribe() to send the current state of each block as a
snapshot instead. Non-block milestone events (researchComplete, end,
error) are still replayed so the client knows if the session finished
while it was disconnected.

Fixes ItzCrazyKns#938
Venice.ai uses an OpenAI-compatible API at https://api.venice.ai/api/v1.
The VeniceLLM class extends OpenAILLM and injects venice_parameters to
disable Venice's built-in web search (enable_web_search: "off") and
system prompt (include_venice_system_prompt: false), since Perplexica
handles web searching and prompt management itself. Models are fetched
dynamically from the Venice API. Configured via VENICE_API_KEY env var.

Made-with: Cursor
LM Studio and most OpenAI-compatible APIs don't support OpenAI's
structured output feature (zodResponseFormat / chat.completions.parse).

This adds a generateObject override that:
- Uses standard chat.completions.create() with response_format JSON mode
- Includes the JSON schema in the prompt for guidance
- Integrates repairJson for robust parsing
- Falls back to prompt-only JSON instruction for models without JSON mode

This enables features like follow-up suggestions to work with LM Studio,
vLLM, LocalAI, and other OpenAI-compatible inference servers.

Contributed by The Noble House™ AI Lab (https://thenoblehouse.ai)
Some LLM providers (Claude, models via LiteLLM/OpenRouter) wrap JSON
output in markdown code fences (\\\json ... \\\). The streamObject()
paths in both OpenAI and Ollama providers pass accumulated text directly
to partial-json's parse(), which fails on the fence characters.

Add stripMarkdownFences() and safeParseJson() utilities in
src/lib/utils/parseJson.ts. Applied to:
- streamObject() in OpenAI and Ollama providers (partial JSON parsing)
- generateText() tool call argument parsing in OpenAI provider

The existing generateObject() paths already use repairJson() with
extractJson: true, which handles this case.

Fixes ItzCrazyKns#959

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When the LLM returns incomplete tool arguments (e.g. missing the
queries or urls field), calling .slice() on undefined crashes the
entire search. Default to an empty array so execution continues
gracefully.

Fixes ItzCrazyKns#964
Truncate the HTML to 200K chars before passing it to Turndown so we
don't waste CPU converting huge pages we mostly discard after
tokenization anyway.
Scraped web pages were being sent to the LLM in full, with no
truncation. A single large page could produce 100K+ tokens of markdown,
easily exceeding the model's context window.

Use the existing splitText utility to cap scraped content at ~6000 tokens
per page. Also add per-result and total character limits when assembling
the final context for the writer prompt.

Fixes ItzCrazyKns#1031
…#1054)

Wraps search pipeline in try/catch to emit error events instead of
leaving the UI stuck on "Brainstorming". Persists failed messages
with status: 'error'.

Cherry-picked from upstream PR ItzCrazyKns#1054 (stablegenius49)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ns#1034)

DeepSeek-r1-671b only emits </think> without <think>, causing thinking
content to leak into the main answer. Prepend <think> when only a
closing tag is found.

Adapted from upstream PR ItzCrazyKns#1034 (MaxwellCalkin)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add null safety checks in convertToOpenAIMessages for tool call
  arguments that may be strings or objects
- Wrap tool call argument parsing in try-catch to handle malformed
  JSON gracefully, falling back to empty object on error
- Add guard against undefined/empty queries in webSearch action
- Add fallback for undefined chatHistory in researcher
- Ensure SearXNG always returns arrays even on empty responses

These fixes prevent crashes when OpenRouter/compatible providers
return unexpected data formats during streaming tool calls.
Add check for empty content before calling repairJson to prevent
"is empty" error when the model returns null/empty response.
…bility

Replace OpenAI-exclusive APIs with standard endpoints that work across
OpenAI-compatible providers (OpenRouter, LiteLLM, etc.):

- generateObject: Use chat.completions.create with response_format
  instead of chat.completions.parse (returns 404 on OpenRouter)
- streamObject: Use chat.completions.create with streaming instead of
  responses.stream (OpenAI Responses API not supported by other providers)

Also adds shared parseJson utility for stripping markdown code fences
that LLMs sometimes wrap around JSON responses even with json_object mode.

Fixes ItzCrazyKns#959
Non-2xx responses now throw so the error propagates to callers (which
already have try-catch). Also wrapped res.json() in a try-catch to
handle cases where a 2xx response has malformed/non-JSON body.
This addresses several reported issues in one pass:

- Remove hardcoded num_ctx (32000) in the Ollama provider. It now uses
  the contextWindowSize option from GenerateOptions, letting Ollama fall
  back to whatever the model was loaded with instead of forcing 32K.
  (fixes ItzCrazyKns#981)

- Add null-safety checks on response.message in both OpenAI and Ollama
  providers. Some endpoints (vLLM, older Ollama builds) can return
  responses where message or content is missing, causing "Cannot read
  properties of undefined" crashes. (fixes ItzCrazyKns#836, ItzCrazyKns#789)

- Normalize YouTube embed URLs from youtube-nocookie.com to youtube.com
  in the video search widget. Some SearXNG instances return the
  privacy-enhanced domain which doesn't resolve on certain networks.
  (fixes ItzCrazyKns#799)

- Handle SearXNG errors gracefully instead of crashing. Added HTTP
  status check in searchSearxng(), try-catch wrappers in webSearch,
  academicSearch, socialSearch, and video search actions, and early
  returns on empty results. The UI no longer hangs indefinitely when
  SearXNG returns a CAPTCHA or error. (fixes ItzCrazyKns#763)

- Add console.error logging for scrape failures, search failures, and
  SearXNG errors so issues are visible in the terminal. (fixes ItzCrazyKns#997)
…CrazyKns#1065)

Fixes "independant" -> "independent", "converastion" -> "conversation",
and removes trailing comma in JSON example that could cause parse failures.

From upstream PR ItzCrazyKns#1065 (northern-64bit)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
safeParseJson returns unknown — add generic type param and fallback
to empty object to satisfy ToolCall type.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 18, 2026 21:48
@coderabbitai
Copy link

coderabbitai bot commented Mar 18, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 34ac5d20-39c8-40bb-b7bc-2fd4a1698723

📥 Commits

Reviewing files that changed from the base of the PR and between 57f0ab7 and c9db1d0.

📒 Files selected for processing (8)
  • src/app/api/chat/route.ts
  • src/app/api/reconnect/[id]/route.ts
  • src/lib/agents/search/context.ts
  • src/lib/agents/search/index.ts
  • src/lib/config/index.ts
  • src/lib/models/providers/lmstudio/lmstudioLLM.ts
  • src/lib/models/providers/ollama/ollamaLLM.ts
  • src/lib/models/providers/openai/openaiLLM.ts

📝 Walkthrough

Walkthrough

Adds SSE keep-alives and robust stream shutdown; hardens search researcher actions and web-scraping (SSRF checks, redirects, truncation); introduces token-aware search-result context builder; adds Venice model provider and LLM integration; improves JSON parsing utilities and token utilities; changes session replay behavior and various LLM provider JSON/stream handling.

Changes

Cohort / File(s) Summary
Ignore & Config
/.gitignore, src/lib/config/index.ts
Ignore added for /.claude/settings.local.json. Config env loading for search now trims values, validates searxngURL, skips blank env entries, and applies defaults when current values are falsy.
SSE Streaming
src/app/api/chat/route.ts, src/app/api/reconnect/[id]/route.ts
Add 15s keep-alive, immediate initial keepAlive, streamClosed guard, safeWrite/safeClose helpers, and use safe shutdown on errors/abort/disconnect.
Search Agent Core
src/lib/agents/search/api.ts, src/lib/agents/search/index.ts
Wrap search flow in try/catch, centralized error logging + session error emission, use buildSearchResultsContext, and make DB message updates transactional on update path.
Search Context
src/lib/agents/search/context.ts
Add buildSearchResultsContext(searchFindings: Chunk[]) to produce token-aware, truncated XML-like <result> entries joined by newlines.
Researcher Actions
src/lib/agents/search/researcher/...
Multiple actions hardened: safe (input.queries ?? []) handling, try/catch around searchSearxng, guards for empty results; scrapeURL adds URL safety (DNS/IP checks), manual redirect handling, HTML/token truncation, markdown conversion safeguards; webSearch schema loosened and now errors when all queries fail.
Media & Searxng
src/lib/agents/media/video.ts, src/lib/searxng.ts
Video search wrapped in try/catch; YouTube embed URL normalization. SearXNG fetch adds X-Forwarded-For/X-Real-IP headers, improved non-OK/non-JSON errors, and default empty arrays for missing fields.
Session & Hooks
src/lib/session.ts, src/lib/hooks/useChat.tsx
Subscription replay changed to emit block snapshots plus selective milestone events (researchComplete, end, error). Chat hook ignores keepAlive messages and ensures missing opening <think> tags are prepended.
LLM Providers — Venice
src/lib/models/providers/venice/index.ts, src/lib/models/providers/venice/veniceLLM.ts, src/lib/models/providers/index.ts
New Venice provider and VeniceLLM: provider metadata/config UI, model discovery via Venice API, config validation, loadChatModel, and provider registration; VeniceLLM injects venice_parameters into JSON requests.
LLM Providers — Enhancements
src/lib/models/providers/... (groq, lmstudio, ollama, openai)
Add/extend generateObject/stream handling with JSON-mode prompts, JSON repair, markdown-fence stripping, safer partial/streamed JSON parsing, optional contextWindow sizing, and more robust optional chaining.
Models & Types
src/lib/models/types.ts
Add optional contextWindowSize?: number to GenerateOptions.
Utilities
src/lib/utils/parseJson.ts, src/lib/utils/splitText.ts
Add stripMarkdownFences and safeParseJson; export getTokenCount and add truncateTextByTokens for token-aware truncation.
Other Fixes & Small Changes
src/lib/agents/media/video.ts, src/lib/agents/search/researcher/index.ts, src/lib/prompts/search/classifier.ts, src/lib/models/providers/index.ts
Minor defensive checks, typo fixes, logging improvements, export of Venice provider in registry.
Sessional & Flow Changes
src/lib/session.ts, src/lib/agents/search/api.ts
Event replay semantics changed (block snapshot + selective events); search flow now emits research/widget/results within try/catch and skips DB finalization on error.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Server as SSE Route
  participant Session
  participant DB

  Client->>Server: GET /api/chat or /api/reconnect
  Server->>Session: subscribe(writer)
  Server->>Server: start keepAliveInterval (every 15s)
  Note right of Server: send immediate keepAlive
  Session-->>Server: emit 'block' / 'researchComplete' / 'data'
  Server->>Server: safeWrite(JSON + \n)
  Server-->>Client: SSE event (block/researchComplete/response)
  alt writer error or client abort
    Server->>Server: safeClose() -> clear keepAlive, writer.close()
    Server->>Session: disconnect() (conditional)
    Server->>DB: update messages/status as needed
    Server-->>Client: SSE 'end' or connection closed
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Poem

🐰 I hummed a tiny heartbeat line,
I fenced the JSON, trimmed the vine,
Venice sails with keys in hand,
Searches safe across the land.
Keep-alives hop — the chat's alive!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Scenario 1: Provider compatibility, search hardening, Venice.ai' directly addresses the three primary objectives of the PR: provider compatibility updates, search pipeline hardening, and Venice.ai integration.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch scenario1-batch-ab-venice
📝 Coding Plan
  • Generate coding plan for human review comments

Comment @coderabbitai help to get the list of available commands and usage tips.

@Eternalhazed
Copy link
Owner Author

@claude do a code review

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR consolidates multiple upstream changes focused on improving OpenAI-compatible provider interoperability (OpenRouter/LiteLLM/Together/Groq/LM Studio/Ollama), hardening the search/scrape pipeline (SSRF protections, timeouts, context budgeting, better reconnect behavior), and adding Venice.ai as a first-class provider.

Changes:

  • Reworked structured-output and tool-call handling to rely on chat.completions.create, with JSON fence stripping + JSON repair across providers.
  • Hardened search and scraping (SSRF checks, error surfacing, token budgets, keepalive frames, safer reconnect snapshots).
  • Added Venice.ai provider integration with dynamic model listing.

Reviewed changes

Copilot reviewed 27 out of 28 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/lib/utils/splitText.ts Exported token counting + added token-based truncation helper for search context budgeting
src/lib/utils/parseJson.ts New helpers to strip markdown fences and safely parse JSON from LLM outputs
src/lib/session.ts Reconnect subscribers now get block snapshots to avoid duplicated content on reconnect
src/lib/searxng.ts Adds headers + improved error/JSON handling for SearXNG fetch
src/lib/prompts/search/classifier.ts Prompt spelling fixes + valid JSON example cleanup
src/lib/models/types.ts Added contextWindowSize option for provider compatibility (e.g., Ollama)
src/lib/models/providers/venice/veniceLLM.ts Venice OpenAI-compatible LLM wrapper with request-body parameter injection
src/lib/models/providers/venice/index.ts Venice provider config + model discovery + model loading
src/lib/models/providers/openai/openaiLLM.ts Replaced parse/Responses API usage; improved JSON/tool-call robustness across providers
src/lib/models/providers/ollama/ollamaLLM.ts Makes num_ctx configurable + JSON fence stripping for object generation
src/lib/models/providers/lmstudio/lmstudioLLM.ts Overrides generateObject to support LM Studio via JSON mode + schema prompting
src/lib/models/providers/index.ts Registers Venice provider
src/lib/models/providers/groq/groqLLM.ts Restores Groq structured output via json_object mode
src/lib/hooks/useChat.tsx DeepSeek missing <think> guard + ignore keepalive frames
src/lib/config/index.ts Env var precedence + URL validation for searxngURL
src/lib/agents/search/researcher/index.ts Guards against undefined chat history slicing
src/lib/agents/search/researcher/actions/webSearch.ts Guards empty queries + surfaces SearXNG failures without crashing
src/lib/agents/search/researcher/actions/uploadsSearch.ts Guards undefined queries before slicing
src/lib/agents/search/researcher/actions/socialSearch.ts Guards undefined queries + catch SearXNG failures
src/lib/agents/search/researcher/actions/scrapeURL.ts SSRF protection + redirect validation + HTML/token caps
src/lib/agents/search/researcher/actions/academicSearch.ts Guards undefined queries + catch SearXNG failures
src/lib/agents/search/index.ts Adds error surfacing to session + token-budgeted context building
src/lib/agents/search/context.ts New token-budgeted search context builder with per-result caps
src/lib/agents/search/api.ts Uses new context builder + wraps pipeline in try/catch to emit error events
src/lib/agents/media/video.ts Handles SearXNG errors + normalizes YouTube embed URLs
src/app/api/reconnect/[id]/route.ts Adds keepalive frames + safe write/close wrappers for reconnect streaming
src/app/api/chat/route.ts Adds keepalive frames + safe write/close wrappers for chat streaming
.gitignore Ignores local Claude settings file

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 43 to 48
if (msg.role === 'tool') {
return {
role: 'tool',
tool_call_id: msg.id,
content: msg.content,
tool_call_id: msg.id || '',
content: msg.content || '',
} as ChatCompletionToolMessageParam;
Comment on lines 53 to 64
...(msg.tool_calls &&
msg.tool_calls.length > 0 && {
tool_calls: msg.tool_calls?.map((tc) => ({
id: tc.id,
id: tc.id || '',
type: 'function',
function: {
name: tc.name,
arguments: JSON.stringify(tc.arguments),
name: tc.name || '',
arguments:
typeof tc.arguments === 'string'
? tc.arguments
: JSON.stringify(tc.arguments || {}),
},
Comment on lines +192 to +199
} catch (parseErr) {
console.error('Error parsing tool call arguments:', parseErr, 'tc:', JSON.stringify(tc));
parsedToolCalls.push({
name: tc.function?.name || '',
id: tc.id || recievedToolCalls[tc.index]?.id || '',
arguments: {},
});
}
text: {
format: zodTextFormat(input.schema, 'object'),
},
stream: true,
Comment on lines +47 to +50
headers: {
'X-Forwarded-For': '127.0.0.1',
'X-Real-IP': '127.0.0.1',
},
)
) {
throw error;
}
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 18

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/app/api/chat/route.ts`:
- Around line 162-186: The safeWrite and safeClose functions currently use
try/catch around writer.write() and writer.close() but those return promises and
can reject asynchronously; change safeWrite and safeClose to handle the async
rejections by either making them async and awaiting writer.write(...) and
writer.close(...) inside try/await/catch blocks or by attaching .catch(...) to
the returned promises, and on any write/close failure set streamClosed = true
and clear the keepAliveInterval to stop further writes; update references to
writer, encoder, keepAliveInterval, streamClosed, safeWrite, and safeClose
accordingly so promise rejections are observed and the keep-alive timer is
cleared when the stream errors.

In `@src/app/api/reconnect/`[id]/route.ts:
- Around line 55-87: The listener passed to session.subscribe synchronously
calls disconnect() for terminal events while disconnect is assigned after
subscribe returns, causing a Temporal Dead Zone; fix by declaring a disconnect
placeholder before calling session.subscribe (e.g., let disconnect = () => {}),
then assign the real unsubscribe function returned from session.subscribe to
that identifier so the callback can safely call disconnect() (ensure you update
the existing session.subscribe usage and retain calls to safeWrite/safeClose in
the 'end' and 'error' branches).

In `@src/lib/agents/media/video.ts`:
- Line 52: The console.error call in the video search path currently logs raw
user input via res.query; update the error logging in the video search logic
(the console.error that prints `Video search failed for query "${res.query}"`)
to avoid persisting raw queries by either redacting the query (e.g. replace with
"<REDACTED_QUERY>") or logging a non-reversible fingerprint (hash) of res.query,
and include contextual fields like error message and any non-sensitive
identifiers instead of the plain query string.

In `@src/lib/agents/search/api.ts`:
- Around line 41-44: The code calls Promise.all([widgetPromise, searchPromise])
even when classification.classification.skipSearch is true and searchPromise is
null; replace this implicit behavior by ensuring searchPromise is always a real
Promise (e.g., resolve to null via Promise.resolve(null)) or branch to await
widgetPromise alone; update the logic around widgetPromise and searchPromise
(the variables named widgetPromise and searchPromise and the check
classification.classification.skipSearch) so Promise.all only receives Promises
(use Promise.resolve(null) for skipped search) or skip Promise.all and await
widgetPromise directly to make intent explicit.
- Around line 29-44: The researcher is being started with
SessionManager.createSession() which creates a detached session and prevents
real-time progress events from reaching the main client; change the call site
where researcher.research(...) is invoked (the researcher.research call in this
file) to pass the main session variable (session) instead of
SessionManager.createSession(), so that progress blocks emitted by Researcher
are routed to the existing session and the client receives intermediate updates
as well as the final searchResults.

In `@src/lib/agents/search/context.ts`:
- Around line 9-10: The code uses escapeAttribute but leaves raw '&' and emits
unquoted XML for index, allowing crafted finding.content or title to break the
pseudo-XML; update escapeAttribute (and add an escapeText function) to fully
escape XML entities (&, <, >, ", ') for attribute values and plain text nodes,
ensure the index is emitted as a quoted attribute (e.g., index="..."), and apply
these escapes to all places that wrap content/title/index into pseudo-XML
(including the blocks referenced around escapeAttribute and the other
occurrences at lines 36-48) before any token counting or truncation so the model
receives well-formed, safe XML-like input.

In `@src/lib/agents/search/index.ts`:
- Around line 33-54: The delete-then-update on the messages table (using
db.delete(messages).where(and(eq(messages.chatId, input.chatId), gt(messages.id,
exists.id))) and db.update(messages).set({...}).where(and(eq(messages.chatId,
input.chatId), eq(messages.messageId, input.messageId)))) must be executed
inside a single transaction to avoid race conditions; modify the code to open a
transaction (e.g., db.transaction(...)) and perform both the delete and the
update within that transaction, committing only after both operations succeed
and rolling back on error, while preserving use of session.id, exists.id,
input.chatId and input.messageId.

In `@src/lib/agents/search/researcher/actions/scrapeURL.ts`:
- Around line 126-143: The current try/catch around dns.lookup swallows all DNS
errors allowing the request to proceed; change the error handling in the block
around dns.lookup so that any DNS resolution failure causes the function to fail
closed: after calling dns.lookup (in the try block that assigns resolved), keep
the existing isBlockedIPAddress check and throw for private addresses, but in
the catch block rethrow any error that is not the deliberate "Refusing to access
local or private network URL" error (or simply always throw a new Error
referencing rawURL and the original error) instead of silently returning; update
the catch to reference dns.lookup, resolved, isBlockedIPAddress and rawURL so
DNS failures cannot be bypassed.
- Around line 58-77: The isBlockedIPv6 function's string-prefix checks are
insufficient and miss ranges; update isBlockedIPv6 (and use normalizeAddress) to
properly detect IPv6 ranges by parsing the address (e.g., via a reliable IP
library or by expanding/normalizing to full hex) and then check numeric/prefix
ranges for fe80::/10 (link-local), ff00::/8 (multicast), and 2001:db8::/32
(documentation) in addition to the existing fc00/ fd00 (ULA) checks; for
IPv4-mapped addresses keep the existing isBlockedIPAddress call for the mapped
part and ensure all comparisons are done on a parsed/normalized representation
rather than brittle string startsWith checks.

In `@src/lib/agents/search/researcher/actions/webSearch.ts`:
- Around line 121-128: The search function currently swallows all
searchSearxng() errors and returns, making outages indistinguishable from
legitimate no-results and logging raw queries (PII); change search to propagate
failures instead of returning silently: catch errors from searchSearxng(q), log
a non-PII, context-only message (e.g., mention "SearXNG search failed" and an
identifier or sanitized/hash of q rather than the raw query), and rethrow or
return an explicit error object so the upstream pipeline can surface the
failure; reference the search function and the call to searchSearxng to locate
where to implement this change.

In `@src/lib/config/index.ts`:
- Around line 235-241: The validation for f.key === 'searxngURL' currently only
checks URL syntax via new URL(envValue) but doesn't enforce scheme; update the
check in the block that handles f.key === 'searxngURL' to parse the URL (using
new URL(envValue)) and then explicitly verify url.protocol is either 'http:' or
'https:' and return (reject) for any other protocol, ensuring only HTTP(S) is
allowed.

In `@src/lib/models/providers/lmstudio/lmstudioLLM.ts`:
- Around line 30-35: Validate that input.messages is a non-empty array at the
start of the function (or before the blocks using it) and throw or return a
clear error if empty; extract const lastMessage =
input.messages[input.messages.length - 1] and use lastMessage instead of
repeated indexing when building messagesWithJsonInstruction and in the second
block (lines 91–100) to avoid accessing undefined.content; ensure both places
reuse lastMessage and handle the empty case early to prevent runtime errors.
- Around line 62-69: Extract the duplicated JSON repair/parse/validate flow into
a single helper (e.g. parseAndValidateJson<T>(content: string, schema:
ZodType<T>) or similar) that runs repairJson(..., { extractJson: true }),
JSON.parse, and schema.parse and throws a consistent, descriptive Error on
failure; then replace the duplicated blocks in the two methods that currently
perform this flow (the blocks around the JSON.parse -> schema.parse sequence at
the ranges shown) to call this helper and return its result, preserving generic
typing and error propagation.

In `@src/lib/models/providers/ollama/ollamaLLM.ts`:
- Around line 96-98: The generateObject() and streamObject() methods are missing
the num_ctx option (which is set in generateText()/streamText()), so add
num_ctx: input.options?.contextWindowSize ??
this.config.options?.contextWindowSize to the options objects passed in both
generateObject() and streamObject() so structured-output flows use the
configured context window (match how generateText/streamText apply num_ctx).

In `@src/lib/models/providers/openai/openaiLLM.ts`:
- Around line 169-200: The recievedToolCalls handling is inconsistent: it checks
recievedToolCalls[tc.index] but uses recievedToolCalls.push(call), causing index
mismatches and sparse-array bugs; change the logic in the loop so you assign by
index instead of pushing — i.e., when creating a new call set
recievedToolCalls[tc.index] = call (not push) and when merging use
recievedToolCalls[tc.index] directly to append to .arguments, then parse
recievedToolCalls[tc.index].arguments into parsedToolCalls; keep the same
fallback id/name/arguments logic and preserve the parse error catch as-is.
- Around line 300-314: The current streamObject loop yields an empty object on
JSON parse failure (yield {} as T) which violates the declared
AsyncGenerator<Partial<z.infer<T>>>; instead remove the empty-object fallback
and validate parsed objects against the schema (use input.schema) before
yielding: after computing cleanedObj (via stripMarkdownFences) attempt JSON
parse, then run schema.parse/parseAsync or safeParse and only yield the
validated result; if parsing or validation fails, either log and continue
skipping that chunk or re-throw the error (do not yield {}) so callers must
handle failures—update the block around chunk.choices, parse,
stripMarkdownFences, and input.schema accordingly.

In `@src/lib/models/providers/venice/index.ts`:
- Around line 59-70: The getModelList method uses a non-null assertion on
getConfiguredModelProviderById(this.id) which can be undefined; update
getModelList to safely handle a missing provider by calling
getConfiguredModelProviderById(this.id) into a variable (configProvider), check
if it's undefined, and then either throw a clear error or gracefully fallback
(e.g., return only default models) before accessing
configProvider.embeddingModels and configProvider.chatModels; adjust references
to getDefaultModels, getModelList and this.id accordingly so the function no
longer relies on the ! operator.
- Around line 31-57: The getDefaultModels method currently calls fetch and
res.json without error handling; wrap the fetch/res.json logic in a try/catch,
check res.ok after the fetch and throw or return a clear error/empty ModelList
if the Venice API returns non-2xx, and guard against invalid JSON or missing
data by validating that data && Array.isArray(data.data) before iterating;
reference the getDefaultModels function and this.config.apiKey when adding these
checks, and ensure the method still returns a ModelList (embedding: [], chat:
defaultChatModels) on handled failure rather than letting an unhandled exception
bubble.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 73458ec6-8550-4f51-9de6-de3abc1e391e

📥 Commits

Reviewing files that changed from the base of the PR and between b02f5aa and 7780960.

📒 Files selected for processing (28)
  • .gitignore
  • src/app/api/chat/route.ts
  • src/app/api/reconnect/[id]/route.ts
  • src/lib/agents/media/video.ts
  • src/lib/agents/search/api.ts
  • src/lib/agents/search/context.ts
  • src/lib/agents/search/index.ts
  • src/lib/agents/search/researcher/actions/academicSearch.ts
  • src/lib/agents/search/researcher/actions/scrapeURL.ts
  • src/lib/agents/search/researcher/actions/socialSearch.ts
  • src/lib/agents/search/researcher/actions/uploadsSearch.ts
  • src/lib/agents/search/researcher/actions/webSearch.ts
  • src/lib/agents/search/researcher/index.ts
  • src/lib/config/index.ts
  • src/lib/hooks/useChat.tsx
  • src/lib/models/providers/groq/groqLLM.ts
  • src/lib/models/providers/index.ts
  • src/lib/models/providers/lmstudio/lmstudioLLM.ts
  • src/lib/models/providers/ollama/ollamaLLM.ts
  • src/lib/models/providers/openai/openaiLLM.ts
  • src/lib/models/providers/venice/index.ts
  • src/lib/models/providers/venice/veniceLLM.ts
  • src/lib/models/types.ts
  • src/lib/prompts/search/classifier.ts
  • src/lib/searxng.ts
  • src/lib/session.ts
  • src/lib/utils/parseJson.ts
  • src/lib/utils/splitText.ts

Comment on lines +29 to +44
let searchPromise: Promise<ResearcherOutput> | null = null;

if (!classification.classification.skipSearch) {
const researcher = new Researcher();
searchPromise = researcher.research(SessionManager.createSession(), {
chatHistory: input.chatHistory,
followUp: input.followUp,
classification: classification,
config: input.config,
});
}

const [widgetOutputs, searchResults] = await Promise.all([
widgetPromise,
searchPromise,
]);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check what events the researcher emits that might be lost
ast-grep --pattern 'session.emit($$$)'

Repository: Eternalhazed/perplexica

Length of output: 1453


🏁 Script executed:

fd -e ts -e js 'researcher' | head -20

Repository: Eternalhazed/perplexica

Length of output: 103


🏁 Script executed:

# Find the Researcher class definition
rg 'class Researcher' -A 20

Repository: Eternalhazed/perplexica

Length of output: 1529


🏁 Script executed:

# Check if Researcher uses session.emit in its methods
rg 'this\.session\.emit|session\.emit' src/lib/agents/search

Repository: Eternalhazed/perplexica

Length of output: 806


🏁 Script executed:

cat -n src/lib/agents/search/researcher/index.ts | head -100

Repository: Eternalhazed/perplexica

Length of output: 3713


🏁 Script executed:

# Get the specific lines where session.emitBlock is called
rg 'session\.emitBlock' src/lib/agents/search/researcher/index.ts -B 3 -A 3

Repository: Eternalhazed/perplexica

Length of output: 370


🏁 Script executed:

# See how index.ts passes the session to researcher
rg -B 5 -A 5 'researcher\.research' src/lib/agents/search/index.ts

Repository: Eternalhazed/perplexica

Length of output: 453


🏁 Script executed:

# Check what the research block contains - understand the full impact
cat -n src/lib/agents/search/researcher/index.ts | sed -n '99,120p'

Repository: Eternalhazed/perplexica

Length of output: 975


🏁 Script executed:

# Check the actual api.ts code at the lines mentioned in the review
cat -n src/lib/agents/search/api.ts | sed -n '29,44p'

Repository: Eternalhazed/perplexica

Length of output: 717


🏁 Script executed:

# Also check what comes before to see the full context
cat -n src/lib/agents/search/api.ts | sed -n '1,50p'

Repository: Eternalhazed/perplexica

Length of output: 2063


Detached session for researcher loses progress updates.

On line 33, researcher.research() is called with SessionManager.createSession() instead of the main session parameter. This creates a separate session that receives all intermediate research progress blocks emitted by the researcher (reasoning steps, source findings, etc.), but these events never reach the client connected via the main session.

Compare this with src/lib/agents/search/index.ts which correctly passes session to the researcher. While the API version does emit the final searchResults to the main session (lines 47-50), the real-time research progress updates are discarded. The client receives only the final results, not the step-by-step research process that the standard search agent would provide.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/agents/search/api.ts` around lines 29 - 44, The researcher is being
started with SessionManager.createSession() which creates a detached session and
prevents real-time progress events from reaching the main client; change the
call site where researcher.research(...) is invoked (the researcher.research
call in this file) to pass the main session variable (session) instead of
SessionManager.createSession(), so that progress blocks emitted by Researcher
are routed to the existing session and the client receives intermediate updates
as well as the final searchResults.

Comment on lines +300 to 314
if (chunk.choices && chunk.choices.length > 0) {
const delta = chunk.choices[0].delta.content || '';
receivedObj += delta;

// Strip markdown fences if present
const cleanedObj = stripMarkdownFences(receivedObj);

try {
yield parse(recievedObj) as T;
yield parse(cleanedObj) as T;
} catch (err) {
console.log('Error parsing partial object from OpenAI:', err);
// Partial JSON may not be parseable yet, yield empty object
yield {} as T;
}
} else if (chunk.type === 'response.output_text.done' && chunk.text) {
try {
yield parse(chunk.text) as T;
} catch (err) {
throw new Error(`Error parsing response from OpenAI: ${err}`);
}
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how streamObject consumers handle partial/empty yields
rg -n "streamObject" --type ts -A 5

Repository: Eternalhazed/perplexica

Length of output: 1538


🏁 Script executed:

# Get the full streamObject implementation in openaiLLM.ts
cat -n src/lib/models/providers/openai/openaiLLM.ts | sed -n '269,320p'

Repository: Eternalhazed/perplexica

Length of output: 2192


🏁 Script executed:

# Get the generateObject implementation to see schema validation
rg -n "generateObject" src/lib/models/providers/openai/openaiLLM.ts -A 30 | head -60

Repository: Eternalhazed/perplexica

Length of output: 1551


🏁 Script executed:

# Find all consumers/callers of streamObject
rg -n "\.streamObject\(" --type ts -B 2 -A 5

Repository: Eternalhazed/perplexica

Length of output: 49


🏁 Script executed:

# Check the base class definition more thoroughly
cat -n src/lib/models/base/llm.ts

Repository: Eternalhazed/perplexica

Length of output: 831


🏁 Script executed:

# Find the parse function definition
rg -n "function parse\|const parse\|export.*parse" --type ts -B 2 -A 8 | head -50

Repository: Eternalhazed/perplexica

Length of output: 49


🏁 Script executed:

# Search for imports of streamObject or its usage pattern
rg -n "streamObject" --type ts

Repository: Eternalhazed/perplexica

Length of output: 370


🏁 Script executed:

# Check if there's schema validation happening with zod
rg -n "\.parse\(" src/lib/models/providers/openai/openaiLLM.ts -B 2 -A 2

Repository: Eternalhazed/perplexica

Length of output: 321


🏁 Script executed:

# Check imports and parse function in openaiLLM.ts
head -50 src/lib/models/providers/openai/openaiLLM.ts

Repository: Eternalhazed/perplexica

Length of output: 1440


🏁 Script executed:

# Search for parse function definition anywhere
rg -n "^function parse|^const parse|^export.*parse" --type ts -A 5

Repository: Eternalhazed/perplexica

Length of output: 49


🏁 Script executed:

# Look for JSON-repair package or similar
rg -n "repair\|parse" src/lib/models/providers/openai/openaiLLM.ts -B 1 -A 2 | head -40

Repository: Eternalhazed/perplexica

Length of output: 49


🏁 Script executed:

# Check the GenerateObjectInput type to see if schema is available in streamObject
rg -n "type GenerateObjectInput\|interface GenerateObjectInput" --type ts -A 10

Repository: Eternalhazed/perplexica

Length of output: 49


🏁 Script executed:

# See full generateObject implementation to understand schema validation
cat -n src/lib/models/providers/openai/openaiLLM.ts | sed -n '215,268p'

Repository: Eternalhazed/perplexica

Length of output: 2516


Remove empty object fallback and validate streamed objects against schema.

Streaming partial JSON without schema validation creates a type contract violation: the base class declares AsyncGenerator<Partial<z.infer<T>>>, but this implementation yields {} as T on parse failures (line 311), which consumers won't expect. Unlike generateObject, streamObject performs no schema validation despite having access to input.schema. Consider either:

  • Validating each parsed object against input.schema before yielding (catching validation errors appropriately)
  • Or removing the empty object fallback and re-throwing parse errors after logging, forcing consumers to handle failures explicitly
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/models/providers/openai/openaiLLM.ts` around lines 300 - 314, The
current streamObject loop yields an empty object on JSON parse failure (yield {}
as T) which violates the declared AsyncGenerator<Partial<z.infer<T>>>; instead
remove the empty-object fallback and validate parsed objects against the schema
(use input.schema) before yielding: after computing cleanedObj (via
stripMarkdownFences) attempt JSON parse, then run schema.parse/parseAsync or
safeParse and only yield the validated result; if parsing or validation fails,
either log and continue skipping that chunk or re-throw the error (do not yield
{}) so callers must handle failures—update the block around chunk.choices,
parse, stripMarkdownFences, and input.schema accordingly.

Security fixes:
- SSRF: DNS lookup now fails closed instead of swallowing errors
- SSRF: Block IPv6 multicast addresses (ff00::/8)
- XML injection: Proper entity escaping in search context builder
- SearXNG URL: Enforce http/https protocol allowlist
- OpenAI: Fix tool call index mismatch (push → index assignment)
- OpenAI: Filter incomplete tool messages instead of empty defaults

Async/error handling:
- Stream writes: Use .catch() on promises instead of sync try/catch
- Reconnect: Fix Temporal Dead Zone in session.subscribe callback
- Ollama: Add num_ctx to generateObject/streamObject methods
- Venice: Add error handling for API fetch + remove non-null assertion
- SearXNG: Surface search outages instead of silent empty results
- PII: Remove raw user queries from error logs
- Promise.all: Use explicit Promise.resolve(null) for skipped search
- DB: Wrap delete+update in transaction for atomicity
- LM Studio: Guard against empty input.messages
- streamObject: Skip unparseable chunks instead of yielding {}
- OpenAI streamObject: Add response_format json_object mode
- Tool call logging: Remove full payload, log only name/index

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/app/api/chat/route.ts (1)

158-267: ⚠️ Potential issue | 🟠 Major

Hoist the cleanup handles out of the try block.

The keep-alive timer and session subscription are created before later setup steps can still throw, but safeClose and disconnect are both scoped inside the try. If startup fails after they are created, the catch path cannot clear the timer or unsubscribe the session, so the request leaves background work behind. Move those handles to the outer scope, call them from catch, and only arm keep-alives once setup has succeeded.

♻️ Suggested fix
 export const POST = async (req: Request) => {
+  let safeClose: (() => void) | undefined;
+  let disconnect: (() => void) | undefined;
   try {
     const reqBody = (await req.json()) as Body;
@@
-    const safeClose = () => {
+    safeClose = () => {
       if (streamClosed) return;
@@
-    keepAliveInterval = setInterval(() => {
-      safeWrite({ type: 'keepAlive' });
-    }, keepAliveMs);
-
-    safeWrite({ type: 'keepAlive' });
-
-    const disconnect = session.subscribe((event: string, data: any) => {
+    disconnect = session.subscribe((event: string, data: any) => {
       if (event === 'data') {
         ...
       }
     });
@@
     req.signal.addEventListener('abort', () => {
-      disconnect();
-      safeClose();
+      disconnect?.();
+      safeClose?.();
     });
+
+    if (!streamClosed) {
+      keepAliveInterval = setInterval(() => {
+        safeWrite({ type: 'keepAlive' });
+      }, keepAliveMs);
+      safeWrite({ type: 'keepAlive' });
+    }
 
     return new Response(responseStream.readable, {
       headers: {
         'Content-Type': 'text/event-stream',
@@
   } 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' },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/api/chat/route.ts` around lines 158 - 267, Hoist the cleanup handles
and subscription out of the try: declare keepAliveInterval, streamClosed,
safeWrite, safeClose and the disconnect variable (result of session.subscribe)
in the outer scope before entering the try, but do not start the keep-alive
timer or call session.subscribe until setup succeeds; inside the try only
assign/arm them (setInterval and subscribe) after
agent.searchAsync/ensureChatExists succeed; add cleanup calls in the
catch/finally to clearInterval(keepAliveInterval), call safeClose(), and call
disconnect() or remove the subscription so the timer and session are always
cleaned up if startup throws; reference the existing symbols keepAliveInterval,
safeWrite, safeClose, disconnect, session.subscribe, and
req.signal.addEventListener to implement this.
src/app/api/reconnect/[id]/route.ts (1)

19-116: ⚠️ Potential issue | 🟠 Major

Make the reconnect cleanup reachable from catch.

The keep-alive timer is started before session.subscribe(...) finishes replaying state, but the cleanup handles live inside the try. If replay/setup throws, the catch path cannot clear the timer and may miss tearing down partially initialized state. Hoist the cleanup handles outside the try, and only start keep-alives after subscription setup has succeeded.

♻️ Suggested fix
 export const POST = async (
   req: Request,
   { params }: { params: Promise<{ id: string }> },
 ) => {
+  let safeClose: (() => void) | undefined;
+  let disconnect: (() => void) | undefined;
   try {
@@
-    const safeClose = () => {
+    safeClose = () => {
       if (streamClosed) return;
@@
-    keepAliveInterval = setInterval(() => {
-      safeWrite({ type: 'keepAlive' });
-    }, keepAliveMs);
-
-    safeWrite({ type: 'keepAlive' });
-
-    let disconnect: (() => void) | undefined;
-
     disconnect = session.subscribe((event, data) => {
       if (event === 'data') {
         ...
       }
     });
@@
     req.signal.addEventListener('abort', () => {
-      disconnect();
-      safeClose();
+      disconnect?.();
+      safeClose?.();
     });
+
+    if (!streamClosed) {
+      keepAliveInterval = setInterval(() => {
+        safeWrite({ type: 'keepAlive' });
+      }, keepAliveMs);
+      safeWrite({ type: 'keepAlive' });
+    }
 
     return new Response(responseStream.readable, {
       headers: {
         'Content-Type': 'text/event-stream',
@@
   } catch (err) {
+    disconnect?.();
+    safeClose?.();
     console.error('Error in reconnecting to session stream: ', err);
     return Response.json(
       { message: 'An error has occurred.' },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/api/reconnect/`[id]/route.ts around lines 19 - 116, The keep-alive
timer and cleanup handles (keepAliveInterval, streamClosed, safeWrite,
safeClose, disconnect) are created inside the try so the catch can't teardown
partially-initialized state; hoist declarations for keepAliveInterval,
streamClosed and the safeWrite/safeClose/disconnect variables outside the try,
call session.subscribe(...) and only after subscribe returns successfully start
the setInterval keepAlive via keepAliveInterval = setInterval(...), and ensure
the req.signal.addEventListener('abort', ...) references the hoisted disconnect
and safeClose so the catch path can clear the interval and close the writer if
setup throws.
src/lib/models/providers/openai/openaiLLM.ts (1)

225-246: ⚠️ Potential issue | 🔴 Critical

Include input.schema in the system message for both structured-output methods.

Since switching off chat.completions.parse, these methods no longer tell the model what schema to produce. The response_format: { type: 'json_object' } enforces valid JSON syntax only—the model has no guidance on structure. input.schema.parse() becomes post-hoc validation against a schema the model never saw, regressing from schema-driven generation to best-effort prompting.

🔧 Suggested change
+    const schemaInstruction = JSON.stringify(
+      z.toJSONSchema(input.schema),
+      null,
+      2,
+    );
+
     const response = await this.openAIClient.chat.completions.create({
       messages: [
         {
           role: 'system',
           content:
-            'You must respond with valid JSON only. No markdown code blocks, no explanatory text.',
+            `You must respond with valid JSON only. No markdown code blocks, no explanatory text.\n\nReturn an object matching this JSON Schema:\n${schemaInstruction}`,
         },
         ...this.convertToOpenAIMessages(input.messages),
       ],

Apply the same change in streamObject().

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/models/providers/openai/openaiLLM.ts` around lines 225 - 246, The
system prompt sent to the model (in openAIClient.chat.completions.create)
enforces JSON-only output via response_format but does not include input.schema,
so the model lacks structural guidance; update the system message assembly in
both the completion path (where convertToOpenAIMessages is used) and the
streamObject() path to append or embed input.schema (e.g., its JSON/schema
string or a short instruction derived from it) so the model is explicitly
instructed to produce the schema-defined JSON structure before calling
response_format: { type: 'json_object' } and then validate with
input.schema.parse() as currently done.
♻️ Duplicate comments (3)
src/lib/agents/media/video.ts (1)

52-52: ⚠️ Potential issue | 🟠 Major

Sanitize error logging to prevent query leakage via upstream error messages.

Line 52 still risks logging user query text indirectly, because searchSearxng throws messages that include query. Log a static message plus safe metadata, not error.message verbatim.

🔧 Proposed fix
-    console.error('Video search failed:', error instanceof Error ? error.message : error);
+    console.error('Video search failed', {
+      errorName: error instanceof Error ? error.name : 'UnknownError',
+    });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/agents/media/video.ts` at line 52, Replace the current console.error
call that logs error.message (which can contain user queries) with a sanitized
log: emit a static message like "Video search failed" plus only safe metadata
(e.g., error.name, a short numeric error code, and/or a correlation id) and do
NOT include error.message or full stack; if you need full details for
diagnostics, send the original error to your secure error-tracking service
(e.g., Sentry) after scrubbing sensitive fields. Apply this change around the
video search call that uses searchSearxng so the log no longer leaks query text.
src/lib/agents/search/api.ts (1)

32-39: ⚠️ Potential issue | 🟠 Major

Detached session still loses researcher progress updates.

The researcher is instantiated with SessionManager.createSession() (line 33) instead of the main session parameter. This creates a disconnected session, so intermediate research progress (reasoning steps, source findings) never reaches the client. Compare with src/lib/agents/search/index.ts line 88 which correctly passes session to the researcher.

🔧 Proposed fix
       if (!classification.classification.skipSearch) {
         const researcher = new Researcher();
-        searchPromise = researcher.research(SessionManager.createSession(), {
+        searchPromise = researcher.research(session, {
           chatHistory: input.chatHistory,
           followUp: input.followUp,
           classification: classification,
           config: input.config,
         });
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/agents/search/api.ts` around lines 32 - 39, The Researcher is being
created with a detached session via SessionManager.createSession() causing
progress updates to never reach the client; replace the detached session with
the existing main session passed into this function by instantiating Researcher
(or calling researcher.research) with session instead of
SessionManager.createSession(), i.e., change the call that constructs/passes the
session to use the function parameter session so searchPromise (and
Researcher.research) emits intermediate progress to the client.
src/lib/agents/search/context.ts (1)

42-54: ⚠️ Potential issue | 🟡 Minor

Content should be XML-escaped before insertion.

The finding.content is inserted verbatim into the pseudo-XML structure without escaping. A scraped page containing </result> or similar text could corrupt the structure passed to the model. Apply escapeXmlText to the content.

🛡️ Proposed fix
-    const fullContent = String(finding.content || '');
+    const fullContent = escapeXmlText(String(finding.content || ''));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/agents/search/context.ts` around lines 42 - 54, The raw
finding.content is being inserted into the pseudo-XML without escaping; use the
escapeXmlText helper to prevent injected XML from breaking the structure. After
you compute/truncate the text (variables fullContent, content, and when you
build the truncated variant with TRUNCATION_NOTE), pass the final content string
through escapeXmlText and use that escaped value when constructing entry (prefix
+ escapedContent + suffix). Ensure token counting and truncation still operate
on the original fullContent before escaping.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/lib/agents/search/index.ts`:
- Around line 96-99: The Promise.all call currently passes widgetPromise and
searchPromise directly; when searchPromise can be null you should mirror api.ts
and replace searchPromise with (searchPromise ?? Promise.resolve(null)) so
Promise.all always receives promises; update the array in the Promise.all
invocation that assigns to [widgetOutputs, searchResults] to use this explicit
Promise.resolve fallback for searchPromise (referencing the widgetPromise and
searchPromise variables in this file).

In `@src/lib/config/index.ts`:
- Around line 235-243: When f.key === 'searxngURL' and envValue is present but
invalid, the current try/catch just returns leaving any previously persisted
search.searxngURL unchanged; change this to fail-closed by explicitly resetting
the config value (e.g., set config.search.searxngURL = '' or undefined or a
known safe default) when the URL parse or protocol check fails. Update the block
that references envValue and parsed so that on catch or invalid protocol you
assign the safe default to the same target that would be set on success (refer
to f.key 'searxngURL', envValue, parsed and the config property
search.searxngURL) instead of returning early.

In `@src/lib/models/providers/lmstudio/lmstudioLLM.ts`:
- Around line 18-25: LMStudioLLM currently overrides generateObject() but leaves
streamObject() inheriting OpenAILLM.streamObject(), which uses only
response_format: { type: 'json_object' } and will fail for LM Studio; override
LMStudioLLM.streamObject() to mirror generateObject() behavior by inserting the
schema into the prompt, setting response_format with the LM Studio-compatible
fallback (e.g., response_format that prefers structured JSON but allows raw JSON
text), implement streaming parsing that accumulates chunks and
extracts/validates the JSON object per item type T, and fall back to manual JSON
parsing if the structured response isn't provided; ensure you reference
LMStudioLLM.streamObject, LMStudioLLM.generateObject, and the base
OpenAILLM.streamObject when making the change.
- Around line 78-82: The catch block in generateObject (the try/catch that
currently checks `err.message?.includes('response_format') || err.status ===
400`) is too permissive; restrict the fallback to only when the error explicitly
indicates an unsupported response_format. Replace the condition with a precise
check for the response_format-related text (e.g.,
`err.message?.includes('response_format') ||
err.response?.data?.message?.includes('response_format')`) and remove the
`err.status === 400` branch so only explicit response_format errors call
`generateObjectWithoutJsonMode<T>(input, jsonSchema)`.

In `@src/lib/models/providers/ollama/ollamaLLM.ts`:
- Around line 263-270: The loop in streamObject() appends chunk.message.content
unconditionally which lets undefined or empty chunks corrupt recievedObj or
cause duplicate parses; change the loop to guard for truthy content on each
chunk (check chunk?.message?.content) before appending to recievedObj, applying
stripMarkdownFences and calling parse(cleanedObj) — only yield when content was
actually appended; reference recievedObj, streamObject(), stripMarkdownFences,
and parse to locate the changes.

In `@src/lib/models/providers/openai/openaiLLM.ts`:
- Around line 257-265: The console.error in the repairJson catch block currently
prints the full model reply variable content, which can leak sensitive data;
remove that raw dump and instead log a redacted or non-sensitive indicator
(e.g., a fixed message, a content length, or a hash/ID) when repairJson fails.
Update the catch for repairErr surrounding repairJson(...) in the openaiLLM code
(the repairedJson variable and repairJson call) to avoid printing content
directly: replace console.error('repairJson failed on content:', content) with a
safe log like console.error(`repairJson failed: length=${content?.length}`) or
compute a short hash/ID, then rethrow or wrap repairErr as currently done.
- Around line 180-205: The code currently synthesizes empty tool arguments with
"|| '{}'" and by pushing an empty-args object in the catch branch, which can
turn a partially-streamed tool call into an executable no-arg call; change the
logic in the block that handles streaming tool calls (references:
recievedToolCalls, parsedToolCalls, tc, parse) to NOT use the "{}" fallback and
to NOT push a parsedToolCalls entry when arguments are missing or JSON parsing
fails — instead store/append the raw argument fragment on
recievedToolCalls[tc.index] and return/continue so the code will attempt parsing
again when more stream data arrives; only push to parsedToolCalls after
parse(args) succeeds. Ensure the catch only logs the parse error (using
parseErr) and leaves the partial entry for later completion rather than emitting
an empty arguments object.

---

Outside diff comments:
In `@src/app/api/chat/route.ts`:
- Around line 158-267: Hoist the cleanup handles and subscription out of the
try: declare keepAliveInterval, streamClosed, safeWrite, safeClose and the
disconnect variable (result of session.subscribe) in the outer scope before
entering the try, but do not start the keep-alive timer or call
session.subscribe until setup succeeds; inside the try only assign/arm them
(setInterval and subscribe) after agent.searchAsync/ensureChatExists succeed;
add cleanup calls in the catch/finally to clearInterval(keepAliveInterval), call
safeClose(), and call disconnect() or remove the subscription so the timer and
session are always cleaned up if startup throws; reference the existing symbols
keepAliveInterval, safeWrite, safeClose, disconnect, session.subscribe, and
req.signal.addEventListener to implement this.

In `@src/app/api/reconnect/`[id]/route.ts:
- Around line 19-116: The keep-alive timer and cleanup handles
(keepAliveInterval, streamClosed, safeWrite, safeClose, disconnect) are created
inside the try so the catch can't teardown partially-initialized state; hoist
declarations for keepAliveInterval, streamClosed and the
safeWrite/safeClose/disconnect variables outside the try, call
session.subscribe(...) and only after subscribe returns successfully start the
setInterval keepAlive via keepAliveInterval = setInterval(...), and ensure the
req.signal.addEventListener('abort', ...) references the hoisted disconnect and
safeClose so the catch path can clear the interval and close the writer if setup
throws.

In `@src/lib/models/providers/openai/openaiLLM.ts`:
- Around line 225-246: The system prompt sent to the model (in
openAIClient.chat.completions.create) enforces JSON-only output via
response_format but does not include input.schema, so the model lacks structural
guidance; update the system message assembly in both the completion path (where
convertToOpenAIMessages is used) and the streamObject() path to append or embed
input.schema (e.g., its JSON/schema string or a short instruction derived from
it) so the model is explicitly instructed to produce the schema-defined JSON
structure before calling response_format: { type: 'json_object' } and then
validate with input.schema.parse() as currently done.

---

Duplicate comments:
In `@src/lib/agents/media/video.ts`:
- Line 52: Replace the current console.error call that logs error.message (which
can contain user queries) with a sanitized log: emit a static message like
"Video search failed" plus only safe metadata (e.g., error.name, a short numeric
error code, and/or a correlation id) and do NOT include error.message or full
stack; if you need full details for diagnostics, send the original error to your
secure error-tracking service (e.g., Sentry) after scrubbing sensitive fields.
Apply this change around the video search call that uses searchSearxng so the
log no longer leaks query text.

In `@src/lib/agents/search/api.ts`:
- Around line 32-39: The Researcher is being created with a detached session via
SessionManager.createSession() causing progress updates to never reach the
client; replace the detached session with the existing main session passed into
this function by instantiating Researcher (or calling researcher.research) with
session instead of SessionManager.createSession(), i.e., change the call that
constructs/passes the session to use the function parameter session so
searchPromise (and Researcher.research) emits intermediate progress to the
client.

In `@src/lib/agents/search/context.ts`:
- Around line 42-54: The raw finding.content is being inserted into the
pseudo-XML without escaping; use the escapeXmlText helper to prevent injected
XML from breaking the structure. After you compute/truncate the text (variables
fullContent, content, and when you build the truncated variant with
TRUNCATION_NOTE), pass the final content string through escapeXmlText and use
that escaped value when constructing entry (prefix + escapedContent + suffix).
Ensure token counting and truncation still operate on the original fullContent
before escaping.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: d02550b1-c7bd-4b50-82bc-124b5554cea1

📥 Commits

Reviewing files that changed from the base of the PR and between 7780960 and 57f0ab7.

📒 Files selected for processing (13)
  • src/app/api/chat/route.ts
  • src/app/api/reconnect/[id]/route.ts
  • src/lib/agents/media/video.ts
  • src/lib/agents/search/api.ts
  • src/lib/agents/search/context.ts
  • src/lib/agents/search/index.ts
  • src/lib/agents/search/researcher/actions/scrapeURL.ts
  • src/lib/agents/search/researcher/actions/webSearch.ts
  • src/lib/config/index.ts
  • src/lib/models/providers/lmstudio/lmstudioLLM.ts
  • src/lib/models/providers/ollama/ollamaLLM.ts
  • src/lib/models/providers/openai/openaiLLM.ts
  • src/lib/models/providers/venice/index.ts

Comment on lines +18 to +25
class LMStudioLLM extends OpenAILLM {
/**
* Generate a structured object response from the LLM.
*
* Uses standard chat completion with JSON mode instead of OpenAI's
* structured output feature for compatibility with LM Studio.
*/
async generateObject<T>(input: GenerateObjectInput): Promise<T> {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "lmstudioLLM.ts" -type f

Repository: Eternalhazed/perplexica

Length of output: 117


🏁 Script executed:

find . -name "*OpenAI*" -type f | grep -E "\.(ts|js)$" | head -20

Repository: Eternalhazed/perplexica

Length of output: 49


🏁 Script executed:

cat -n ./src/lib/models/providers/lmstudio/lmstudioLLM.ts

Repository: Eternalhazed/perplexica

Length of output: 6502


🏁 Script executed:

rg "class OpenAILLM" --type ts

Repository: Eternalhazed/perplexica

Length of output: 159


🏁 Script executed:

cat -n ./src/lib/models/providers/openai/openaiLLM.ts

Repository: Eternalhazed/perplexica

Length of output: 13602


streamObject() still inherits the incompatible base implementation.

LMStudioLLM overrides generateObject() with LM Studio-specific handling (schema in prompt, response_format with fallback), but streamObject() remains unoverridden and inherits OpenAILLM.streamObject(), which relies solely on response_format: { type: 'json_object' } without schema integration or fallback. This may cause structured streaming to fail silently or produce unspecified output for LM Studio.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/models/providers/lmstudio/lmstudioLLM.ts` around lines 18 - 25,
LMStudioLLM currently overrides generateObject() but leaves streamObject()
inheriting OpenAILLM.streamObject(), which uses only response_format: { type:
'json_object' } and will fail for LM Studio; override LMStudioLLM.streamObject()
to mirror generateObject() behavior by inserting the schema into the prompt,
setting response_format with the LM Studio-compatible fallback (e.g.,
response_format that prefers structured JSON but allows raw JSON text),
implement streaming parsing that accumulates chunks and extracts/validates the
JSON object per item type T, and fall back to manual JSON parsing if the
structured response isn't provided; ensure you reference
LMStudioLLM.streamObject, LMStudioLLM.generateObject, and the base
OpenAILLM.streamObject when making the change.

Critical:
- Include input.schema in system message for generateObject/streamObject
  so models know what JSON structure to produce (not just "valid JSON")

Security:
- Redact repairJson error log (was dumping full model reply)
- Escape search result content in pseudo-XML (prevents structure injection)

Robustness:
- Hoist cleanup handles (safeClose/disconnect) out of try blocks in
  chat and reconnect routes so catch can clean up on setup failure
- Defer keepalive timer until after setup succeeds
- SearXNG URL: reset to default on invalid env (fail-closed)
- LM Studio: restrict fallback to explicit response_format errors only
- Ollama streamObject: guard empty chunk.message.content
- Tool call streaming: don't synthesize empty args on parse failure,
  accumulate fragments and retry on next chunk
- Promise.all consistency in search index.ts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Eternalhazed Eternalhazed merged commit 9605203 into master Mar 19, 2026
1 check was pending
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants