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
Artifactssection.
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
ResearchArtifactentries. 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 outsideallowed_toolsand 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_toolsdefaults 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_permissionsdefaults toFalse. Setting itTrueadds--dangerously-skip-permissions, which bypasses every permission check and makesallowed_toolsa 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
claudeCLI installed and authenticated locally - Restricted to a read-only research toolset by default; broaden
allowed_tools(or enableskip_permissionsin a sandbox) for tasks that need more - Non-deterministic results
- The
stream-jsonoutput 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
providerslisting, - 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:
timeoutis handed to the Biomni agent as its owntimeout_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
biomniextra - 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_timeoutapplies to individual LLM HTTP requests. Scientific-tool execution and the whole investigation have no wall-clock deadline;ProviderConfig.timeoutis rejected rather than reinterpreted as a request timeout. This makes ToolUniverse unavailable for the run while preserving other configured providers. Usemax_stepsto 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:
artifact_exclude_globs— an explicit deny always wins.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; setsave_artifacts=Falsefor that.- The report body the provider already returned as the result markdown.
artifact_include_globs— an explicit allow bypasses every remaining default.- Default denies: scaffolding directories (
.git/,.claude/,node_modules/, …), nested archives, and — unlessartifact_keep_runtime— runtime records. - The extension allowlist, plus
artifact_extra_extensions, plus anyimage/*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 …reportsapt-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(...)