Skip to content

Deep Research

Deep research models run minutes-to-an-hour server-side agentic jobs that plan, search the web, and return long cited reports. llmist exposes them through a first-class surface — client.research — with one normalized event stream and job lifecycle across providers.

import { LLMist } from "llmist";
const client = new LLMist();
const job = client.research.start({
model: "gemini:deep-research-preview-04-2026",
query: "What is the state of solid-state batteries in 2026?",
});
for await (const event of job) {
switch (event.type) {
case "phase": console.error(`Phase: ${event.phase}`); break;
case "search": console.error(`Searching: ${event.query ?? ""}`); break;
case "thinking": break; // reasoning summaries
case "text": process.stdout.write(event.delta); break;
case "citation": console.error(`Source: ${event.citation.url}`); break;
}
}
const result = await job.result();
console.log(result.report, result.citations, result.usage.costUSD);

| | OpenAI | Gemini | OpenRouter | |---|---|---|---| | API | Responses API | Interactions API | chat completions | | Models | gpt-5.5-pro (durable), o3-deep-research / o4-mini-deep-research (shut down 2026-07-23) | deep-research-preview-04-2026, deep-research-max-preview-04-2026 (preview, paid tiers) | perplexity/sonar-deep-research, perplexity/sonar-pro-search, openai/*-deep-research | | Streaming | ✅ (gpt-5.5-pro: poll-only) | ✅ | ✅ (mandatory) | | Background jobs | ✅ | ✅ (mandatory) | ❌ | | Resume after disconnect | ✅ (starting_after) | ✅ (last_event_id) | ❌ — a dropped stream is money lost | | Follow-ups (previousJobId) | ❌ | ✅ | ❌ | | Extra pricing dimensions | $10/1k searches | $14/1k searches (post-free-tier) | per-search fees + separately-priced internal reasoning tokens |

Discover what's available at runtime with client.research.listModels() — capabilities, pricing (including perThousandSearches and internalReasoning), and lifecycle metadata come from the research model catalog, and requests are validated against it before any network call. Models past their announced shutdown date throw a typed error naming the replacement.

Every research job yields a normalized ResearchEvent union: created (job id), status, phase (planning/searching/reasoning/writing), search (query/url activity), tool, thinking (reasoning summaries), text (report deltas), citation, usage, error, and done. Each event may carry a cursor (the provider's resume token) and a rawEvent escape hatch with the original payload.

The stream may be consumed once; job.result() aggregates everything (report text, deduplicated citations, usage with search counts and estimated costUSD) and can also be awaited without iterating.

On resumable providers, transient stream drops are reconnected automatically from the last cursor (bounded attempts). On OpenRouter, a drop surfaces error { retryable: false } — llmist never silently re-runs a multi-dollar job.

Background jobs: detach, persist, re-attach

Section titled “Background jobs: detach, persist, re-attach”
const job = client.research.start({ model: "openai:gpt-5.5-pro", query: "..." });
for await (const event of job) {
if (event.type === "created") break; // job id is known
}
const ref = job.toRef(); // JSON-serializable
await fs.writeFile("job.json", JSON.stringify(ref));
// ...process restart...
const revived = client.research.attach(JSON.parse(await fs.readFile("job.json", "utf8")));
const result = await revived.result(); // resumes from the cursor

client.research.get(ref) polls status one-shot; client.research.cancel(ref) stops the job server-side.

Abort vs. cancel — read this before relying on either

Section titled “Abort vs. cancel — read this before relying on either”
  • Abort (signal, timeoutMs expiry) tears down the transport only. A background job keeps running (and billing) server-side; its ref stays attachable.
  • Cancel (job.cancel() / client.research.cancel(ref)) stops the job on the server where the provider supports it (OpenAI, Gemini). On OpenRouter it can only abort the transport.

timeoutMs defaults to one hour (and never exceeds a provider's own cap, e.g. Gemini's 60 minutes).

OpenAI — requires at least one data-source tool; llmist injects web_search from the catalog default and maps it to web_search_preview (the GA tool type breaks research models). file_search accepts up to 2 vector store ids; MCP servers must implement the search+fetch interface with requireApproval: "never". maxToolCalls caps cost. gpt-5.5-pro does not stream — the same surface degrades to status heartbeats followed by the full report as a single text event.

Gemini — research runs as provider-managed agents (preview, paid tiers only). Background execution is mandatory, runs cap at 60 minutes server-side, and completed jobs can seed follow-ups via previousJobId. Set reasoning: { includeThinking: false } to suppress thought summaries.

OpenRouter — plain streamed chat completions; ~350K reasoning tokens per sonar-deep-research run are normal, so expect a long thinking phase before report text. Not resumable and no server-side cancel — for salvageability on long runs, log events yourself (or use the CLI's --json mode). The tools option is rejected (tools are managed upstream).

result().usage.costUSD is estimated from catalog pricing and covers dimensions chat pricing doesn't: per-search fees and separately-priced internal reasoning tokens (Perplexity). Typical runs: Gemini ~$1–3 (Max ~$3–7), sonar-deep-research ~$0.50–2, OpenAI o4-mini ~$0.10–1, gpt-5.5-pro substantially more at $30/$180 per M tokens.

@llmist/testing mocks the whole surface — see the testing guide:

import { mockResearch } from "@llmist/testing";
mockResearch("# Report...", {
citations: [{ url: "https://example.com" }],
usage: { searches: 12 },
}).whenMessageContains("batteries").register();