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);Providers
Section titled “Providers”| | 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.
The event stream
Section titled “The event stream”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-serializableawait 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 cursorclient.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,timeoutMsexpiry) 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).
Provider notes
Section titled “Provider notes”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.
Testing
Section titled “Testing”@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();See also
Section titled “See also”llmist deep-research— the CLI command with--background/--resume- Runnable example:
examples/32-deep-research.ts