Skip to content

Provider Reference

Complete reference for all supported research providers.

Overview

Provider Env Variable Strengths Speed
OpenAI OPENAI_API_KEY Most comprehensive Slow
Perplexity PERPLEXITY_API_KEY Real-time web, multiple speeds Fast-Slow
Edison EDISON_API_KEY Scientific literature Slow
Asta ASTA_API_KEY Semantic Scholar-scale literature retrieval + snippets Fast
Consensus CONSENSUS_API_KEY Academic papers Fast
OpenScientist OPENSCIENTIST_API_KEY Autonomous research, PMID citations Very slow
Cyberian (local agents) Agent-based, thorough Very slow
Claude Code (local claude CLI) Agentic web research, no API key Slow
Biomni (upstream local environment) Biomedical co-scientist, runs code Very slow
ToolUniverse TOOLUNIVERSE_API_KEY or OPENAI_API_KEY + optional extra Local co-scientist, scientific tools and Python execution Very slow
DeepER-Med (stub - no API yet) Evidence-based agentic medical research (arXiv:2604.15456) n/a

See Capabilities, Resources & Archetypes for the vocabulary used to describe each provider, including why a conventional deep-research tool is a subset of the co-scientist case.

OpenAI Deep Research

Setup

export OPENAI_API_KEY="your-key"

Models

Model Aliases Description
o3-deep-research-2025-06-26 o3, o3-deep, o3dr Most comprehensive
o4-mini-deep-research-2025-06-26 o4m, o4-mini, mini Balanced speed/quality

Parameters

from deep_research_client.provider_params import OpenAIParams

params = OpenAIParams(
    allowed_domains=["pubmed.ncbi.nlm.nih.gov"],  # Filter to domains
    temperature=0.2,
    max_tokens=4000,
    top_p=0.95
)

Characteristics

  • Cost: High to Very High
  • Speed: 2-15 minutes
  • Context Window: 128K tokens
  • Capabilities: Web search, code interpretation, comprehensive synthesis

Perplexity AI

Setup

export PERPLEXITY_API_KEY="your-key"

Models

Model Aliases Description
sonar-deep-research deep, deep-research, sdr Comprehensive
sonar-pro pro, sp Balanced
sonar basic, fast, s Fastest

Parameters

from deep_research_client.provider_params import PerplexityParams

params = PerplexityParams(
    allowed_domains=["wikipedia.org", "github.com"],
    reasoning_effort="high",  # low, medium, high
    search_recency_filter="month"  # day, week, month, year
)

# Or use native domain filter with deny-list
params = PerplexityParams(
    search_domain_filter=[
        "github.com",       # Allow
        "-reddit.com",      # Deny (prefix with -)
    ]
)

Characteristics

  • Cost: Low to High (depends on model)
  • Speed: Seconds to minutes
  • Context Window: 100K-200K tokens
  • Capabilities: Real-time web search, recent data

Edison Scientific (Falcon)

Setup

export EDISON_API_KEY="your-key"

Models

Model Aliases Description
Edison Scientific Literature falcon, edison, eds, science Scientific papers

Parameters

from deep_research_client.provider_params import FalconParams

params = FalconParams(
    temperature=0.1,
    max_tokens=8000
)

Characteristics

  • Cost: High
  • Speed: 2-5 minutes
  • Capabilities: Scientific literature, powered by PaperQA3
  • Artifacts: Edison output artifacts are fetched from the completed task. Image artifacts such as diagrams, charts, and figures are written beside saved reports and embedded in the generated Markdown; other artifact files are linked from an Artifacts section.

Asta

Setup

export ASTA_API_KEY="your-key"

Models

Model Aliases Description
Asta Scientific Corpus Retrieval asta, retrieval, snippets Retrieval-only paper and snippet lookup

Parameters

from deep_research_client.provider_params import AstaParams

params = AstaParams(
    query_char_limit=500,
    paper_limit=50,
    snippet_limit=20,
    publication_date_range="2021:",
    venues="Nature,Science"
)

Characteristics

  • Cost: Free
  • Speed: Usually a few seconds
  • Capabilities: Scientific literature retrieval, snippet search, direct evidence reporting

Consensus

Setup

export CONSENSUS_API_KEY="your-key"

Note: Requires application approval.

Models

Model Aliases Description
Consensus Academic Search consensus, academic, papers, c Peer-reviewed only

Characteristics

  • Cost: Low
  • Speed: Seconds
  • Capabilities: Academic papers only, evidence-based summaries

OpenScientist

Setup

export OPENSCIENTIST_API_KEY="name:secret"
# Optional: custom instance URL
export OPENSCIENTIST_URL="https://www.openscientist.io"

Important: Your account must be approved by an administrator at openscientist.io before you can create jobs. Until approved, the API returns 403 Forbidden.

Models

Model Aliases Description
openscientist-autonomous openscientist, autonomous-research Iterative hypothesis-driven research

Parameters

from deep_research_client.provider_params import OpenScientistParams

params = OpenScientistParams(
    max_iterations=5,              # Research iterations (1-20)
    use_hypotheses=False,          # Enable hypothesis tracking
    investigation_mode="autonomous",  # "autonomous" or "coinvestigate"
    poll_interval=30,              # Seconds between status checks
    timeout=3600,                  # Max wait time (1-2 hours recommended)
    save_artifacts=True,           # Preserve useful ZIP artifacts
    artifact_max_bytes=5 * 1024 * 1024,  # Per-artifact extraction limit
    artifact_keep_runtime=False,   # Also keep logs, transcripts, stdout/stderr
    artifact_extra_extensions=[],  # Extend the extension allowlist
    artifact_include_globs=[],     # Force-keep these paths
    artifact_exclude_globs=[],     # Always drop these paths
)

See Artifact selection for what the last four do.

Characteristics

  • Cost: Variable (uses Claude under the hood)
  • Speed: 10-60+ minutes (iterative multi-step research)
  • Capabilities: PubMed search, code execution, hypothesis-driven research
  • Citations: PMID format with deduplication
  • Artifacts: Useful figures, small structured files, and rendered reports from the OpenScientist artifact ZIP are returned as ResearchArtifact entries. Runtime scaffolding, logs, transcripts, archives, and oversized files are skipped by default, and every part of that is configurable — see Artifact selection.

When to Use

  • Disease pathophysiology research with PubMed citations
  • Hypothesis-driven scientific literature reviews
  • Biomedical mechanism discovery
  • Evidence synthesis with structured PMID references

Limitations

  • Requires account approval at openscientist.io
  • Very slow (designed for comprehensive research)
  • PubMed-focused (may not cover all scientific domains)
  • Cost scales with number of iterations

Cyberian (Agent-Based)

Setup

pip install deep-research-client[cyberian]

Cyberian uses local AI agents (Claude, Aider, etc.) - no separate API key needed.

Parameters

from deep_research_client.provider_params import CyberianParams

params = CyberianParams(
    agent_type="claude",       # claude, aider, cursor, goose, codex
    workflow_file=None,        # Custom workflow file
    port=3284,                 # agentapi server port
    skip_permissions=True,     # Skip permission checks
    manage_server=True,        # Start/stop agentapi server (set False for external server)
    sources="academic papers", # Source guidance
    workdir_base=None,         # Base directory for workspaces (default: system temp)
    max_iterations=None,       # Limit looping task iterations (requires cyberian >= 0.3.0)
)

If you want to run a pre-configured agentapi server (for example, Codex with yolo mode), start it manually and set manage_server=False so deep-research-client does not restart it.

Characteristics

  • Cost: Variable (depends on agent)
  • Speed: 10-30+ minutes
  • Capabilities: Iterative research, citation management, comprehensive synthesis

When to Use

  • Comprehensive literature reviews
  • Deep technical research
  • Multi-source citation management

Testing and Iteration Control

Use max_iterations to limit how many times looping tasks (like the iterate subtask) can run. This is passed through to cyberian's TaskRunner and requires cyberian >= 0.3.0.

# Limit to 2 iterations for testing
deep-research-client research --provider cyberian "query" \
    --param agent_type=codex \
    --param max_iterations=2

This is useful for:

  • Testing: Run a quick verification without full research
  • Cost control: Limit agent API calls
  • Debugging: Inspect intermediate results after fixed iterations

Claude Code

Claude Code is a local command-line tool rather than an HTTP API. This provider shells out to the claude binary in non-interactive ("print") mode, pipes the research prompt to it via stdin, and lets Claude Code's own agentic tools (web search, web fetch, file reading) carry out the research. The response comes back as a cited markdown report.

Output is read as a stream-json event stream so that every assistant message becomes part of the report. The simpler json output format exposes only a result field holding the agent's final message, so an agent that wrote its report and then emitted any closing remark would lose the whole report while still exiting 0 with valid provenance (#59).

Because every assistant message is kept, any narration the agent emits between tool calls ("Let me search for X…") appears in the report alongside the research. That is a deliberate trade — including narration is cosmetic, whereas selecting a single message risks dropping the report — but it has one consequence worth knowing: citations are extracted from the joined text, so a URL mentioned only in passing is counted in citation_count.

Setup

No API key is needed — authentication and billing are handled by your local Claude Code installation. Just make sure the CLI is installed and on your PATH:

claude --version   # should succeed

The provider is auto-detected whenever claude is found on PATH. Set DISABLE_CLAUDE_CODE_PROVIDER=true to opt out of auto-detection.

Parameters

from deep_research_client.provider_params import ClaudeCodeParams

params = ClaudeCodeParams(
    model="opus",                 # optional; forwarded to `claude --model`
    allowed_tools=["WebSearch", "WebFetch"],  # tool allowlist (default: read-only research set)
    skip_permissions=False,       # default; True bypasses ALL checks (see Security)
    add_dirs=["/data/papers"],    # optional --add-dir entries
    working_dir="/tmp/research",  # optional cwd for the run
    min_report_chars=200,         # fail on an implausibly short report (0 disables)
    extra_args=["--max-turns", "30"],  # escape hatch for unmodeled flags
)

When skip_permissions is False (the default), you can also set permission_mode (e.g. "plan" or "acceptEdits").

Failing loudly on an empty report

A run that produces no research is the expensive failure mode, because it still writes a well-formed file with real cost and provenance metadata — easy to skim past and mistake for a real report. min_report_chars (default 200) turns that into a raised ValueError and a non-zero exit. The rejected text is logged in full and previewed in the exception, so a failed run is diagnosable without paying for a second one.

!!! warning "This default is a behavior change"

Runs that previously returned a short answer successfully now raise. A
one-sentence reply is typically under 200 characters. Set
`min_report_chars=0` if you expect short answers.

This is an emptiness check, not a quality one — a 250-character "I was unable to find sufficient information on this topic" passes it cleanly.

Three run_metadata fields help diagnose a thin result after the fact:

  • assistant_text_blocks — how many separate assistant messages the report was assembled from. More than one is normal for an agentic run; this is provenance for how the report was assembled, not a warning sign.
  • permission_denials — how many tool calls were refused.
  • denied_tools — which tools were refused. A non-zero count often explains a thin report: the agent asked for a tool outside allowed_tools and gave up, and this names what to add.

Security

In non-interactive mode the run is driven by an agent, and get_first_available() may select this provider for an arbitrary query whenever claude is on PATH. To keep that safe by default:

  • allowed_tools defaults to a read-only research set (["WebSearch", "WebFetch"]), passed via --allowedTools. Tools not on the list are auto-denied (without blocking the run), so the agent cannot edit files or run shell commands.
  • skip_permissions defaults to False. Setting it True adds --dangerously-skip-permissions, which bypasses every permission check and makes allowed_tools a no-op (all tools become available). Only enable it in trusted, sandboxed environments.

Widen allowed_tools if a task genuinely needs more. The most common case is research over local documents: the default set is web-only and deliberately omits the Read tool, so to let Claude Code read files you have supplied (for example in an add_dirs path) you must add it explicitly:

params = ClaudeCodeParams(
    allowed_tools=["WebSearch", "WebFetch", "Read"],
    add_dirs=["/data/papers"],
)

Read is left out of the default because it grants the agent read access to the local filesystem, which is unnecessary for purely web-based research and is a mild information-disclosure surface if the query is untrusted. Add it only when reading local documents is actually part of the task.

Usage

deep-research-client research --provider claude_code "your research question"

See a full example report produced by this provider, including the YAML frontmatter that records the actual model(s) used and run provenance (run_metadata).

Characteristics

  • Cost: Handled by your Claude Code subscription / API key
  • Speed: Slow (agentic, multi-step)
  • Capabilities: Web search, citation tracking, code interpretation
  • Auth: None required by this client; relies on local Claude Code

Limitations

  • Requires the claude CLI installed and authenticated locally
  • Restricted to a read-only research toolset by default; broaden allowed_tools (or enable skip_permissions in a sandbox) for tasks that need more
  • Non-deterministic results
  • The stream-json output carries every tool result, including full fetched page bodies, so a fetch-heavy run can buffer tens of MB of stdout in memory. Not a correctness problem, but worth knowing for long runs.

DeepER-Med (Stub)

DeepER-Med is an evidence-based agentic deep medical research framework introduced in Wang et al., DeepER-Med: Advancing Deep Evidence-Based Research in Medicine Through Agentic AI (arXiv:2604.15456, submitted 16 April 2026). The paper describes an open-source paradigm with a public website and agent API, but at the time of writing no code, API endpoint, or dataset has been released publicly.

This provider is registered as a stub so:

  • the wrapper slot is reserved and discoverable via providers listing,
  • model cards and parameter classes are in place,
  • callers asking for it are told why it cannot run, with the arXiv pointer, instead of getting a bare "provider not found".

Behavior

is_available() always returns False, so DeepER-Med is never auto-selected and can never be chosen by get_first_available(). It is still registered unconditionally, which means an explicit request reports the stub status:

client.research("...", provider="deeper_med")
# ValueError: DeepER-Med has no public API or code release yet, so this
# provider cannot run research. See https://arxiv.org/abs/2604.15456 ...

Calling DeeperMedProvider.research() directly raises NotImplementedError with the same message. In the CLI it is listed under Stub providers (not yet callable) in deep-research-client providers.

Caveats

The model card's cost, speed, and capability entries are transcribed from the paper — nothing has been measured, because there is no endpoint to measure. The card's limitations say so explicitly.

Once an API is published, only the body of providers/deeper_med.py needs to change.


Biomni (Biomedical Co-Scientist)

Biomni is a general-purpose biomedical AI agent from Stanford SNAP. Rather than searching the literature and writing a report, it wraps a large toolbox of biomedical software and curated databases and executes generated code to plan and carry out research tasks — designing a CRISPR screen, annotating variants, analysing omics data, and so on. It is a co_scientist archetype: hypothesis-driven and code-running, of which a conventional deep-research run is a subset (see Capabilities, Resources & Archetypes).

This provider wraps the local biomni Python package (biomni.agent.A1). Biomni configures and authenticates its own underlying LLM (Claude by default), so no separate provider API key is required by this client.

Setup

The PyPI package is not Biomni's complete software environment. Follow Biomni's environment setup first: upstream offers a basic agent environment, a reduced environment of about 13GB, and a full E1 setup that it documents as taking more than 10 hours and at least 30GB. Then, inside that activated environment, install this client:

pip install deep-research-client[biomni]

# Biomni drives an LLM under the hood; provide that provider's key, e.g.:
export ANTHROPIC_API_KEY="your-key"

# Optional: where the (~11GB) data lake is stored (default ./biomni_data)
export BIOMNI_DATA_PATH="/data/biomni"

The extra fills gaps in Biomni 0.0.8's package metadata needed for the default agent (pandas, langchain-openai, and langchain-anthropic). It does not install Biomni's R packages, command-line programs, or biomedical Python toolbox; those come from the upstream environment. Anthropic and OpenAI-compatible sources work with the extra. Other backends may need their own adapter, such as langchain-ollama or langchain-aws.

The provider is auto-detected when this core Python runtime is present. Set DISABLE_BIOMNI_PROVIDER=true to opt out of auto-detection.

Important: Biomni executes generated code locally and downloads a large data lake on first run. Run it only in a trusted / sandboxed environment.

Models

Model Aliases Description
biomni-a1 biomni, a1, coscientist Biomni A1 biomedical agent

Note the two model concepts: the model field selects this research model card (biomni-a1), while the llm parameter selects the underlying LLM that Biomni drives.

Parameters

from deep_research_client.provider_params import BiomniParams

params = BiomniParams(
    llm="claude-sonnet-4-20250514",  # underlying LLM (default: Biomni's own)
    source="Anthropic",              # LLM provider: Anthropic, OpenAI, Gemini, ...
    path="/data/biomni",             # workspace root (default: env or ./biomni_data)
    timeout=3600,                    # timeout for each generated code execution
    use_tool_retriever=True,         # retrieve most relevant tools per task
    skip_data_lake=False,            # True skips the ~11GB data lake download
)

Characteristics

  • Cost: Variable (drives an underlying LLM + heavy local compute)
  • Speed: Very slow (multi-step agentic execution)
  • Capabilities: Code execution, data analysis, hypothesis generation, experiment design, evidence synthesis, citation tracking
  • Resources: PubMed, general web, and curated biomedical / genomic / chemical / protein-structure databases
  • Citations: PMIDs, DOIs, PMC and GEO accessions, and bare PubMed URLs, extracted from the final answer with the same patterns reference validation uses
  • Timeout: timeout is handed to the Biomni agent as its own timeout_seconds, which limits each generated code execution. It is not a whole-run deadline; this client does not impose a separate ceiling.

When to Use

  • Designing experiments (e.g. CRISPR screens)
  • Variant annotation and interpretation
  • Omics and sequence data analysis
  • Hypothesis-driven biomedical investigation

Limitations

  • Requires an upstream Biomni environment plus this package's biomni extra
  • Downloads a large (~11GB) data lake on first run
  • Executes generated code locally — use a trusted/sandboxed environment
  • Needs an LLM API key (e.g. ANTHROPIC_API_KEY) for the underlying model
  • Very slow and non-deterministic

ToolUniverse

ToolUniverse provides scientific tools for AI scientists. This wrapper exposes those tools to a smolagents.CodeAgent to accept research questions, investigate hypotheses, execute Python, and return markdown reports. It is a co_scientist, like Biomni and OpenScientist.

Setup

uv add 'deep-research-client[tooluniverse]'
export TOOLUNIVERSE_API_KEY="your-llm-key"
# OPENAI_API_KEY is used if TOOLUNIVERSE_API_KEY is unset.
# Optional: point the LLM at an OpenAI-compatible server.
export TOOLUNIVERSE_BASE_URL="https://your-server.example/v1"

Leave TOOLUNIVERSE_BASE_URL unset to use OpenAI. These are underlying LLM credentials; ToolUniverse does not issue a research-service API key. With Python configuration, pass them as ProviderConfig.api_key and ProviderConfig.base_url. Explicit configuration takes precedence over environment auto-detection.

The extra installs the ToolUniverse SDK and smolagents; it does not install every scientific tool's optional packages. Auto-detection requires both SDKs and an LLM key. Set DISABLE_TOOLUNIVERSE_PROVIDER=true to opt out. Listing providers does not load scientific tools or make an LLM request.

Models and parameters

Model Aliases Description
tooluniverse-coscientist tooluniverse, tu Local scientific CodeAgent

The harmonized model field selects this card. llm selects the actual model used for reasoning and code generation.

Parameter Default Meaning
llm gpt-4.1-mini Underlying OpenAI-compatible model ID
tools See below Non-empty list of exact ToolUniverse tool names
max_steps 20 Agent step limit (1–100); exhausted runs raise an error
request_timeout 120 Timeout in seconds for each LLM HTTP request
workspace None SDK workspace; otherwise TOOLUNIVERSE_HOME or ./.tooluniverse
system_prompt Scientific investigation instructions Custom agent instructions

The default tools are PubMed_search_articles, PubMed_get_article, EuropePMC_search_articles, OpenTargets_get_disease_id_description_by_name, and OpenTargets_get_associated_targets_by_disease_efoId. Only selected tools are exposed to the agent. Unknown or unavailable tool names fail before any LLM request with a provider configuration error, allowing opt-in fallback. Additional tools may require tool-specific environment credentials and packages. allowed_domains is unsupported and rejected; select appropriate tools instead.

The shared toolset also accepts workspace and env_vars when composed into a host. Use an absolute workspace path to share the same SDK configuration across host working directories. The SDK reads an existing workspace's .env and profile.yaml, and seeds a default profile if the directory exists without one. It does not create a missing default workspace. Its persistent cache defaults to ~/.tooluniverse/cache.sqlite; set TOOLUNIVERSE_CACHE_DIR or TOOLUNIVERSE_CACHE_PATH to relocate it.

from deep_research_client import DeepResearchClient
from deep_research_client.models import ProviderConfig

client = DeepResearchClient(provider_configs={
    "tooluniverse": ProviderConfig(
        name="tooluniverse",
        api_key="your-llm-key",
        # base_url="https://your-server.example/v1",
    ),
})
result = await client.research(
    "Investigate therapeutic targets for Parkinson disease and propose experiments.",
    provider="tooluniverse",
    provider_params={"llm": "gpt-4.1-mini", "max_steps": 20},
)
print(result.markdown)
deep-research-client research "Investigate therapeutic targets for Parkinson disease" \
  --provider tooluniverse --param max_steps=20 --param llm=gpt-4.1-mini

Composing ToolUniverse with another agent

tooluniverse is also a shared optional parameter for local agent providers:

Host provider Integration Requirements
claude_code Per-run MCP configuration and explicit tool permissions Local authenticated Claude CLI
biomni A1's add_mcp API Biomni runtime/environment; see its setup above
cyberian MCP configuration in the new workflow workspace agent_type="claude", manage_server=true
openscientist and other hosted APIs Unsupported Requires upstream API support
uv add 'deep-research-client[tooluniverse-tools]'
deep-research-client research "Investigate therapeutic targets for Parkinson disease" \
  --provider claude_code --param tooluniverse=true

The tooluniverse-tools extra installs the scientific SDK and MCP dependencies without smolagents. The tooluniverse extra remains the standalone smolagents agent installation. Composed runs use the host's LLM and authentication; TOOLUNIVERSE_API_KEY and TOOLUNIVERSE_BASE_URL configure only the standalone provider. Individual scientific tools still use their own environment keys. Biomni forwards NCBI_API_KEY by default, alongside MCP's basic process environment such as PATH and HOME. For other scientific credentials, set "env_vars": ["NCBI_API_KEY", "MY_SCIENTIFIC_API_KEY"] inside the toolset object. The temporary configuration stores variable references, not credential values. Include any SDK environment settings such as TOOLUNIVERSE_CACHE_DIR in env_vars too when Biomni's child should inherit them. Claude and Cyberian use their host CLI's inherited environment.

true selects the same default tools as the standalone provider, false/null disables the mixin, and an object selects tools explicitly:

from deep_research_client import DeepResearchClient

client = DeepResearchClient()  # auto-detect the authenticated local Claude CLI
result = await client.research(
    "Investigate therapeutic targets for Parkinson disease.",
    provider="claude_code",
    provider_params={
        "tooluniverse": {"tools": ["PubMed_search_articles", "PubMed_get_article"]},
    },
)

For the CLI, pass that object as JSON:

deep-research-client research "Summarize recent Parkinson disease research" \
  --provider claude_code \
  --param 'tooluniverse={"tools":["PubMed_search_articles","PubMed_get_article"]}'

The shared MCP bridge exposes exactly the configured tools. It preserves input schemas and structured results, with no general dispatcher exposing unselected tools. Claude receives explicit MCP tool permissions alongside its existing allowlist; enabling TU does not turn on permission bypass. Cyberian configures only its fresh workspace. Biomni uses temporary MCP configuration. No global Claude/MCP settings are modified. Tool failures are marked as MCP errors, and both Python and native stdout diagnostics are redirected to stderr to protect the protocol stream. Biomni closes discovery and execution subprocesses after each MCP session.

This integration supplies tools, not TU's skill library or an additional research loop. TU can coexist with skills already installed in the host. Reports retain the host provider identity and record the selected toolset in run metadata; cache keys normalize tool order, default selections, and disabled spellings. Workspace and environment-variable selections remain in cache identity because profiles and credentials can change tool behavior. Cache identity does not track edits to files inside a workspace; bypass or clear cached reports when changing those files. Unsupported hosts reject the parameter, and Cyberian rejects it for unmanaged servers or non-Claude agents.

Limitations

  • Executes generated Python locally; use a trusted environment or run the wrapper inside an external sandbox. Configurable Docker/E2B executors remain a follow-up; the local executor is not a security sandbox.
  • Costs depend on the underlying LLM, tools, and number of steps.
  • request_timeout applies to individual LLM HTTP requests. Scientific-tool execution and the whole investigation have no wall-clock deadline; ProviderConfig.timeout is rejected rather than reinterpreted as a request timeout. This makes ToolUniverse unavailable for the run while preserving other configured providers. Use max_steps to limit agent steps. Cancelling the async caller does not terminate an already running worker thread.
  • Returns the inline markdown report and recognized reference identifiers; generated files are not collected as report artifacts.
  • Available resources and analysis capabilities depend on the selected tools and installed scientific packages. The default tools focus on biomedicine.
  • Biomni starts a fresh MCP process and initializes the SDK for each tool call. This keeps process cleanup predictable but adds per-call startup latency; persistent sessions remain a possible future optimization.

For a local SDK check, run uv run --extra tooluniverse --group dev pytest tests/test_tooluniverse_provider.py. Live PubMed access is covered by the integration tests. A paid LLM integration run additionally requires RUN_TOOLUNIVERSE_INTEGRATION=1 and an LLM key:

RUN_TOOLUNIVERSE_INTEGRATION=1 uv run --extra tooluniverse --group dev \
  pytest -m integration tests/test_tooluniverse_provider.py

Provider Detection

Providers are auto-detected based on environment variables:

# Check available providers
deep-research-client providers

Detection only tells you a provider is configured — an API key is set. It does not tell you the key is still valid, or that the account can pay for a run. To find that out, probe the providers with a cheap live call:

# Probe every configured provider
deep-research-client providers --check

# Probe just one
deep-research-client providers --check --provider falcon

Each provider reports one of OK, UNREACHABLE, NOT CONFIGURED, or UNKNOWN (no probe available) — the last meaning that provider has not implemented a probe, so configuration is all we know. The command exits non-zero if any provider turns out to be unable to take work, which includes a named provider that is not configured at all (no probe needed to know that).

A probe proves the credential is accepted. It cannot prove the account has credits: Edison, for example, only charges when a task is submitted, so an uncredited key passes the probe and fails the run with 402. A numeric balance has to come from the provider's own dashboard.

Provider Failures

Failures are raised as typed exceptions so callers can tell "switch provider" apart from "try again":

Exception Statuses Retryable Means
ProviderAuthError 401, 403 No Key missing, invalid, or lacks access
ProviderBillingError 402 No Account is out of credits
ProviderQuotaError No Plan's usage allowance is spent; carries resets_at when the provider says (bounded by the class, so a trailing on it came from us, not the provider)
ProviderNotConfiguredError No No credential set; nothing was sent, so nothing was rejected
ProviderNotInstalledError No A locally-backed provider's binary is not on PATH, or its optional package is not installed (a kind of "not configured")
ProviderRateLimitError 429 Yes Throttled; wait and retry
ProviderTransientError 5xx Yes Temporary server-side failure

ProviderNotInstalledError subclasses ProviderNotConfiguredError, so one except ProviderNotConfiguredError covers a missing key, a missing CLI, and a missing optional package alike. All of them subclass ProviderError (itself a ValueError, so older callers still work) and carry provider, status_code, detail, and a retryable flag:

from deep_research_client import ProviderBillingError, ProviderError

try:
    result = client.research("...", provider="falcon")
except ProviderBillingError:
    ...  # out of credits: a retry cannot help, pick another provider
except ProviderError as e:
    if e.retryable:
        ...

Auth and billing failures are classified even when a provider SDK retries internally and reports the result as a timeout — the status is recovered from the wrapped exception rather than lost.

OpenAI

OpenAI reports a spent quota as 429 with code: "insufficient_quota" — the same status it uses for ordinary throttling. Reading the status alone would mark a spent quota retryable and loop on it forever, so the body's error code is checked first and only falls back to the status when there is no code we recognise. A model name that does not exist is deliberately left unclassified: that is a caller error, not a provider outage.

Its probe (models.list) is authenticated but not billed, so like Edison it proves the key and nothing more — a key with no quota left still passes. OpenAI exposes no balance endpoint; the spent quota only announces itself on a run.

Claude Code

The claude_code provider is a subprocess wrapper, so it has no status codes to read. Its failures are classified from what the CLI prints — a spent usage allowance, a logged-out session, an expired token, a model the plan does not include, or an overloaded API — and the wordings that mean "stop" are kept apart from the ones that mean "try again".

Its health probe is the most informative of any provider, and the only free one: claude auth status --json reads local credentials and makes no model call, so --check reports the auth method and plan without spending a token.

The one failure the CLI does not always report cleanly is a spent usage limit mid-run, which can stall rather than fail. The timeout message points at providers --check for that reason.

Artifact selection

Providers that return a bundle of files — currently OpenScientist's artifacts ZIP — decide which members become ResearchArtifact entries. The defaults keep what most callers want (figures, small structured data, rendered reports) and drop agent scaffolding, but "noise" is a judgement about the consumer, not about the file: a knowledge base recording how a run reached its answer wants the agent transcripts that a report-only consumer does not.

So the decision is data, not code. ArtifactSelectionParams supplies the knobs and any provider can inherit them:

Parameter Default Effect
save_artifacts True Preserve artifacts at all
artifact_max_bytes 5 MB Per-file size cap
artifact_keep_runtime False Keep logs, agent transcripts, captured stdout/stderr; also allows .log, .txt, .jsonl, .ndjson
artifact_extra_extensions [] Extend the extension allowlist
artifact_include_globs [] Force-keep matching paths
artifact_exclude_globs [] Always drop matching paths

Precedence, highest first:

  1. artifact_exclude_globs — an explicit deny always wins.
  2. artifact_max_bytes — the size cap applies even to an explicit include, because it is what keeps a bundle out of memory. Raise the cap rather than globbing around it — up to 50 MB, which the field enforces as a hard ceiling. A member larger than that cannot be preserved by any setting. The field's minimum is 1, so there is no "keep nothing" setting here; set save_artifacts=False for that.
  3. The report body the provider already returned as the result markdown.
  4. artifact_include_globs — an explicit allow bypasses every remaining default.
  5. Default denies: scaffolding directories (.git/, .claude/, node_modules/, …), nested archives, and — unless artifact_keep_runtime — runtime records.
  6. The extension allowlist, plus artifact_extra_extensions, plus any image/* media type.

Globs are fnmatch patterns matched against the lowercased, bundle-relative path. * crosses /, so *.json matches provenance/iter1_transcript.json.

A comma separates patterns, which means a pattern cannot contain one — [a,b] is a valid fnmatch character class, so artifact_include_globs="data[a,b]/*" becomes two patterns that match nothing. Pass a list to use a comma inside a pattern; a list element is never split. That escape hatch is a Python one: --param splits on the first = only, so every CLI value arrives as a string and is always split on commas.

From Python, where a list setting can be something other than a string, it must be a re-readable collection. A generator or other one-shot iterator is refused rather than read: a params object outlives the policy built from it, so the second reader would get no patterns at all and say nothing about it. bytes is refused for the same reason — iterating it yields integers, not names. Neither shape is reachable from --param, which only ever produces a string.

Extensions are normalized wherever they are set, so csv, .csv and .CSV all mean the same thing; the paths they are matched against are lowercased first, so an unnormalized uppercase spelling would otherwise match nothing. The list-element-never-split rule applies to them as well as to globs, so artifact_extra_extensions=["csv,tsv"] is one extension named .csv,tsv rather than two — pass ["csv", "tsv"] or the string "csv,tsv".

Surrounding whitespace is stripped, and empty and duplicate entries are dropped, from a list as well as from a string. That matters more than it sounds: an empty runtime fragment is a substring of every filename, so a stray blank left by "stderr,".split(",") would otherwise drop the entire bundle.

A params model applies that cleaning to a string but stores a list as given, so OpenScientistParams(artifact_include_globs=" a , , a ") reads back as ["a"] while OpenScientistParams(artifact_include_globs=[" a ", ""]) still reads back as [" a ", ""]. Since --param only ever produces a string, a CLI value is clean at construction; a list from Python is cleaned when the policy is built. Either way the policy's setting is the one that selects.

Keeping OpenScientist agent transcripts

OpenScientist writes its agent transcripts to provenance/iter<N>_transcript.json and provenance/report_transcript.json. They are dropped by default:

params = OpenScientistParams(artifact_keep_runtime=True)

or, to take the transcripts without the container logs:

params = OpenScientistParams(
    artifact_include_globs=["provenance/*_transcript.json"],
)

Both work through the CLI, where list-valued parameters accept a comma-separated string:

deep-research "..." --provider openscientist \
  --param artifact_keep_runtime=true

deep-research "..." --provider openscientist \
  --param 'artifact_include_globs=provenance/*_transcript.json'

Note that transcripts can be large and, being a record of everything the agent did, are worth reading before they are committed anywhere public.

Cached results from before this change

OpenScientist's cache version was bumped when the report-body picker became deterministic and scaffolding-aware, so entries written before that are no longer matched and the next run is live. Nothing is deleted — the old entries stay on disk and list-cache / search-cache still show them — they are just not served.

Falcon's cache version is unchanged. The two used to share one string, so the split keeps Falcon's cached runs, whose behaviour this did not touch.

Transcript statistics

Once transcripts are preserved they can be mined for the shape of a run rather than read line by line:

deep-research-client transcript-stats path/to/provenance/
deep-research-client transcript-stats run_artifacts/ --format text
deep-research-client transcript-stats run_artifacts/ --format json --output stats.json

A directory is searched recursively for *transcript*.json; a named file is read whatever it is called. Several transcripts merge into one summary, which is what you want for a job that writes one per iteration plus one for report generation.

What it reports:

  • Tools — distinct tools called, call counts, failures per tool, MCP server, and summed durations where the transcript records them.
  • Skills — every named skill invoked, with counts.
  • Shell, searches, files — programs run (wrappers and leading environment assignments skipped, so sudo FOO=1 apt-get … reports apt-get), distinct web-search queries, and paths touched by change kind.
  • Models, subagents, tasks, token usage — merged across entry types.
  • Available but unused — tools the session declared at init and never called. The gap is usually more informative than either list alone.
  • Unclassified entries — entries the producer could not classify, and entry types this summarizer has no handling for, counted separately. Both are drift signals; neither is silently dropped.

In Python:

from deep_research_client.transcript_stats import summarize_artifacts

stats = summarize_artifacts(result.artifacts)   # non-transcripts ignored
print(stats.distinct_tools, stats.skills_used)
print(stats.render_markdown())

Tool names are normalized across agent backends: one backend emits mcp__github__search_issues while another emits github.search_issues with a separate namespace field, and both aggregate to search_issues. A dotted prefix is stripped only when it matches the server the entry itself reports, so a tool whose name genuinely contains a dot is left alone.

The summarizer reads decoded JSON and needs no agent SDK installed.

Adding Custom Providers

Create a new provider in src/deep_research_client/providers/:

from . import ResearchProvider
from ..models import ResearchResult

class NewProvider(ResearchProvider):
    async def research(self, query: str) -> ResearchResult:
        # Implementation
        return ResearchResult(...)