Skip to content

feat: add fastCRW (crw) search + scrape provider#240

Open
us wants to merge 5 commits into
danny-avila:mainfrom
us:feat/add-fastcrw
Open

feat: add fastCRW (crw) search + scrape provider#240
us wants to merge 5 commits into
danny-avila:mainfrom
us:feat/add-fastcrw

Conversation

@us

@us us commented Jun 14, 2026

Copy link
Copy Markdown

What

Adds fastCRW (crw) as a first-class provider for both web search (/v1/search) and scraping (/v1/scrape), mirroring the existing tavily-search and firecrawl providers.

Why

This is the engine side that lets @librechat/agents consumers (e.g. LibreChat) actually use fastCRW. Today, selecting a crw provider downstream authenticates but throws here because createSearchTool has no crw branch — this PR adds it.

Why fastCRW over Firecrawl?

  • Runs 100% locally, open core (AGPL). Anti-bot/stealth, BYO-proxy + rotation, and JS rendering all ship in the open-source binary — no cloud flag required. Firecrawl OSS gates its stealth engine behind a cloud-only flag, so a self-hosted Firecrawl instance can't reliably reach protected or JS-heavy sites. fastCRW's self-host can.
  • Faster and more accurate — measured on Firecrawl's own benchmark dataset. Truth-recall 63.74% vs 56.04%; median latency p50 ~1.9 s vs ~2.3 s. Ships as a single ~8 MB Rust binary with ~6 MB RAM idle footprint.
  • Search built on SearXNG, with a quality layer on top. crw is not an alternative to SearXNG — it is built on top of it. SearXNG is the metasearch aggregator underneath; crw adds query expansion (multi-variant rewrite), content-aware reranking (re-scoring by fetched content rather than SearXNG's content-blind ordering), a calibrated direct-answer mode, and category routing (research queries fan out to arxiv / semantic scholar / google scholar, code queries to GitHub). You get SearXNG's breadth plus a measurable accuracy layer, all open-source (AGPL) and self-hostable with configurable engines.
  • Flat, honest pricing. 1 credit = 1 page. No 4× stealth surcharge, no billed-on-failure.

The integration diff is intentionally small because fastCRW exposes a Firecrawl-compatible API — crw closely mirrors the existing firecrawl provider with a configurable base URL.

Changes (additive only — no existing provider touched)

  • src/tools/search/crw-search.ts — search provider (mirrors tavily-search.ts).
  • src/tools/search/crw-scraper.ts — scraper (mirrors firecrawl.ts).
  • src/tools/search/types.tscrw added to the provider enums/types.
  • src/tools/search/tool.ts, search.ts — wired crw into createSearchTool dispatch for search + scrape.
  • src/tools/search/crw.test.ts — unit tests (mocked HTTP, no network) for both search and scrape paths.

Self-host base URL is configurable; key via CRW_API_KEY. Happy to adjust anything to fit your conventions — I maintain the integration and can provide free credits to evaluate.

@us

us commented Jun 14, 2026

Copy link
Copy Markdown
Author

Hi @danny-avila — this is the engine-side companion to the fastCRW PR you Codex-reviewed on LibreChat (danny-avila/LibreChat#13729). Codex correctly pointed out that selecting crw there authenticates but throws, because createSearchTool lives here and has no crw branch. This PR adds it.

What it does

  • crw-search.ts — web search via fastCRW /v1/search (mirrors tavily-search.ts).
  • crw-scraper.ts — scraping via /v1/scrape (mirrors firecrawl.ts).
  • Wired into tool.ts / search.ts / types.ts so crw is a first-class provider for both paths. Additive only — no existing provider touched.

Verified locally (since this repo has no PR CI): tsc --noEmit → 0 errors, and jest src/tools/search/crw.test.ts30/30 passing (search + scrape, all mocked, no network). Diff is +987/−7 across 6 files.

How it lights up LibreChat (the full chain)

  1. merge this → 2. release a new @librechat/agents → 3. bump it in LibreChat + merge #13729 → search works end-to-end.
    Scraping doesn't even need to wait: since fastCRW is Firecrawl-API-compatible, users can already point the existing firecrawl scraper's firecrawlApiUrl at https://fastcrw.com/api today; this PR just makes crw a named, first-class option.

Happy to send a test key (generous credits) so you can try search+scrape once this lands, and glad to adjust anything to fit your conventions. Thanks again for the warm reception on the LibreChat side! 🙌

Copilot AI review requested due to automatic review settings July 1, 2026 14:50

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Adds fastCRW (crw) as an additional first-class provider in @librechat/agents’ search tooling, wiring it into the existing /v1/search + /v1/scrape tool pipeline alongside Serper/SearXNG/Tavily/Firecrawl. This expands the provider abstraction in src/tools/search/ while keeping the public configuration surface consistent with existing providers.

Changes:

  • Extend search/scrape provider types and tool configuration to include crw (API key/URL + options).
  • Add a new CRW search client (createCrwAPI) and CRW scraper (CrwScraper) and wire both into createSearchAPI / createSearchTool.
  • Add Jest coverage for CRW search, scrape normalization, and end-to-end tool wiring (mocked axios).

Reviewed changes

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

Show a summary per file
File Description
src/tools/search/types.ts Adds crw to provider unions and introduces CRW search/scrape config and response types.
src/tools/search/tool.ts Wires crw scraper into tool creation and updates provider documentation + capability flags.
src/tools/search/search.ts Adds crw dispatch branch in createSearchAPI and updates invalid-provider error message.
src/tools/search/crw-search.ts Implements CRW /v1/search client and maps results into the library’s SearchResultData shape.
src/tools/search/crw-scraper.ts Implements CRW /v1/scrape scraper with response normalization into the expected nested data shape.
src/tools/search/crw.test.ts Adds unit tests for CRW search + scrape behavior and verifies tool wiring (search → scrape).

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

Comment on lines +97 to +104
const results: t.SearchResultData = {
organic: organicResults,
images: [],
topStories: [],
videos: [],
news: newsResults,
relatedSearches: [],
};
Comment thread src/tools/search/tool.ts
Comment on lines +335 to +336
* Search providers: Serper (Google results), SearXNG (self-hosted meta-search), Tavily (AI-optimized), fastCRW (Firecrawl-compatible, self-host or cloud).
* Scraper providers: Firecrawl (default, full-featured), Serper (lightweight), Tavily (batch extraction), fastCRW (Firecrawl-compatible, self-host or cloud).
Comment on lines +51 to +55
// Self-host fastCRW may run without auth, so a missing key is only a
// warning — unlike Firecrawl/Tavily, scrapeUrl does NOT early-return on it.
if (!this.apiKey) {
this.logger.warn('CRW_API_KEY is not set. Scraping will not work.');
}
@danny-avila

Copy link
Copy Markdown
Owner

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b3de90cd52

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/tools/search/crw-search.ts Outdated
Comment on lines +90 to +91
const data = body.data ?? {};
const organicResults: t.OrganicResult[] = (data.web ?? [])

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Parse the flat CRW search data array

When /v1/search succeeds, CRW returns data as a flat array of result objects, not an object keyed by web, images, and news. With the current normalization, a real successful response makes data.web, data.images, and data.news all undefined, so CRW searches return empty organic/image/news lists even though the API returned hits.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Pushed back: live-tested 2026-07-02 with a real key, and /v1/search returns a source-keyed object, not a flat array. Both origins agree: POST https://api.fastcrw.com/v1/search and POST https://fastcrw.com/api/v1/search each return {"success":true,"data":{"web":[...]}} (and data.news / data.images for those sources). The source-keyed parsing here was written against those live responses and the provider was live-verified end-to-end (search -> scrape -> formatted output). The earlier flat-array shape this PR originally assumed is exactly what live testing disproved.

Comment on lines +25 to +29
const base = (
apiUrl ??
process.env.CRW_API_URL ??
'https://fastcrw.com/api'
).replace(/\/+$/, '');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Point CRW defaults at the hosted API origin

The hosted CRW API origin is https://api.fastcrw.com, so the current default builds requests like https://fastcrw.com/api/v1/search instead of https://api.fastcrw.com/v1/search. Unless every cloud user overrides crwApiUrl/CRW_API_URL, the new provider sends both search and scraper defaults to the website path rather than the API host.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Fixed in 4704904: both search and scrape defaults now point at https://api.fastcrw.com. Verified live that both endpoints work on that origin (for the record, https://fastcrw.com/api also serves the API and worked live, but the api subdomain is the canonical origin).

Comment on lines +36 to +40
const getSources = async ({
query,
numResults = 8,
type,
}: t.GetSourcesParams): Promise<t.SearchResult> => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Forward CRW date filters

When a caller supplies date through the search tool, this CRW branch drops it while building the request. CRW's /v1/search accepts the same qdr:h|d|w|m|y style filter via tbs, so date-constrained CRW searches currently return unrestricted results even though the tool schema exposes date and the Serper/Tavily providers honor it.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Fixed in 4704904: the search date param is now forwarded as tbs: "qdr:<h|d|w|m|y>". Live-verified the filter is real: a news search for an evergreen query returned 3 results unfiltered and 0 with tbs: "qdr:h". Test added.

proxy: options.proxy ?? this.proxy,
stealth: options.stealth ?? this.stealth,
jsonSchema: options.jsonSchema ?? this.jsonSchema,
timeout: payloadTimeout,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Send scraper deadlines with CRW's request field

When users raise scraperTimeout or crwScraperOptions.timeout for slow JavaScript pages, this sends the value as timeout, but CRW's scrape request uses deadlineMs for the server-side render deadline. The Axios timeout is extended, yet the CRW server can still apply its default deadline and return before the requested scrape budget is used.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Pushed back: timeout is the honored request field, not deadlineMs. Discriminating live test on /v1/scrape: {timeout: 60000, deadlineMs: 50} succeeded (deadlineMs ignored), while {timeout: 50, deadlineMs: 60000} failed (timeout honored). A control request with a bogus unknown field also succeeded, confirming unknown fields are silently ignored. The related real issue (client HTTP timeout racing the server render budget) was already fixed in b3de90c by padding the axios timeout 5s over the payload budget.

const data = raw.data ?? raw;
return {
success: true,
data: {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve JSON scrape results

When a caller enables CRW structured extraction with formats: ['json'] and jsonSchema, successful scrape responses include the extracted data.json, but this normalization whitelists only text/link/metadata fields and drops it. The subsequent extractContent call then sees no usable content, so JSON-only CRW scrapes are reported as successful while producing an empty source.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Resolved differently in 4704904: /v1/scrape rejects formats: ["json"] outright — live response: formats:"json" is not available on /v1/scrape. Use /v1/extract for structured extraction. So there is no data.json to preserve on this endpoint; instead the misleading jsonSchema scrape option was removed entirely, since it could only ever produce an explicit server error. A /v1/extract integration would be a separate feature.

@danny-avila

Copy link
Copy Markdown
Owner

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4704904bb7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

xpath: options.xpath ?? this.xpath,
proxy: options.proxy ?? this.proxy,
stealth: options.stealth ?? this.stealth,
timeout: payloadTimeout,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Send the CRW deadline field for scrape timeouts

When callers raise scraperTimeout or CrwScrapeOptions.timeout for slow/JS-rendered pages, this payload still sends timeout, but CRW's /v1/scrape request uses deadlineMs/deadline_ms for the server-side budget, so the server ignores this field and keeps its default deadline while Axios merely waits longer. That makes configured longer scrape timeouts ineffective for CRW and causes long scrapes to fail early despite the caller opting into a larger timeout.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Pushed back again, now with both casings live-disproven on /v1/scrape: {timeout: 60000, deadlineMs: 50} and {timeout: 60000, deadline_ms: 50} both succeed (the deadline field is ignored — a control with a bogus unknown field also succeeds), while {timeout: 50} and {timeout: 1500, renderJs: true} on a slow page fail with a server-side error envelope, well within the client's HTTP window. So timeout IS the server-side budget field and configured longer timeouts are effective. If you have a spec source for deadlineMs/deadline_ms, please cite it — the live API contradicts it.

Comment thread src/tools/search/crw-search.ts Outdated
// news: [...]}} (live-verified 2026-07-02). OrganicResult.link is a
// REQUIRED string (types.ts), so defend against null/empty urls to never
// feed a broken '' link into the scraper.
const data = body.data ?? {};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Unwrap CRW search results before reading groups

When this is pointed at a current self-hosted CRW /v1/search instance, the success envelope's data is a wrapper with results (the grouped web/news/images payload) rather than the grouped payload itself. Because this line reads body.data directly and then looks for data.web, those searches return success with empty organic/image/news arrays, so searchProvider: 'crw' produces no sources even though CRW returned results.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Cloud is live-verified to return the groups directly under data ({"data":{"web":[...]}} on both api.fastcrw.com and fastcrw.com/api), and I could not verify the self-host data.results wrapper claim (no public spec at standard paths; note the round-1 review claimed data was a flat array, which also contradicts this). That said, a tolerant unwrap is cheap: 51eba56 reads data.results ?? data, with a test for the wrapped shape. Both shapes now work.

@danny-avila

Copy link
Copy Markdown
Owner

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 51eba5676f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +96 to +98
const container = body.data ?? {};
const data = container.results ?? container;
const organicResults: t.OrganicResult[] = (data.web ?? [])

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Handle CRW's array search response

The current fastCRW /v1/search OpenAPI docs define SearchResponse.data as an array of result rows, not an object keyed by web/images/news (see https://docs.fastcrw.com/openapi.json). When the documented { success: true, data: [...] } response is returned, data.web is undefined here, so organicResults, imageResults, and newsResults all come back empty and createSearchTool({ searchProvider: 'crw' }) has no sources to scrape despite a successful search. Normalize flat array responses before reading source-specific buckets.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Handled in ad6a266. For the record, the live cloud API does NOT return the documented flat array — both origins return source-keyed groups ({"data":{"web":[...]}}, re-verified today with a real key) — so the docs' SearchResponse schema appears stale (the same spec's SearchRequest matches live behavior: sources/tbs/limit all verified). But since docs.fastcrw.com/openapi.json documents the flat SearchResult[] shape (with a category field), the normalizer now accepts all three shapes: keyed groups (cloud), flat array routed by category, and the results wrapper. Tests cover each.

xpath: options.xpath ?? this.xpath,
proxy: options.proxy ?? this.proxy,
stealth: options.stealth ?? this.stealth,
timeout: payloadTimeout,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Send CRW scrape deadlines with the documented field

For CRW v1, the public OpenAPI schema exposes deadlineMs as the end-to-end scrape deadline and does not define timeout; in runs that pass scraperTimeout or crwScraperOptions.timeout to allow slow JS pages, this payload field is ignored by CRW while the server keeps its default deadline, so configured longer scrape budgets can still fail early. Map this value to deadlineMs (or include both only if a compatibility target requires it).

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Handled in ad6a266: the payload now sends BOTH timeout and deadlineMs (clamped to the documented 1..60000). Note the live cloud demonstrably honors timeout and ignores deadlineMs in both casings ({timeout:60000, deadlineMs:50} succeeds; {timeout:50} fails server-side), which contradicts the docs — so timeout stays, deadlineMs added for spec-conformant self-host builds. Live-verified the cloud accepts the dual-field payload.

Comment thread src/tools/search/tool.ts
crwScraperOptions?.apiKey ?? crwApiKey ?? process.env.CRW_API_KEY,
apiUrl: crwScraperOptions?.apiUrl ?? crwApiUrl,
timeout: scraperTimeout ?? crwScraperOptions?.timeout,
formats: crwScraperOptions?.formats ?? ['markdown', 'rawHtml'],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Request HTML by default for CRW scraping

With the default createSearchTool({ scraperProvider: 'crw' }) path, this asks CRW for markdown and rawHtml, but CrwScraper.extractContent only runs processContent when response.data.html is present. CRW's v1 schema separates html and rawHtml fields, so default CRW scrapes skip the reference-marker pass and highlights lose link/image/video Core References even though HTML was fetched; request html here or process rawHtml too.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Valid — fixed in ad6a266: default scrape formats are now ['markdown','html'] and extractContent's ref-marker pass falls back to rawHtml when html is absent, so Core References survive any format config. Live-verified: a default-config scrape now returns content WITH references (it previously skipped the ref pass).

@danny-avila

Copy link
Copy Markdown
Owner

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Bravo.

Reviewed commit: ad6a2669d1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

us and others added 5 commits July 9, 2026 11:37
- scrape: read the nested data container (cloud nests like Firecrawl),
  keep top-level fallback for self-host/legacy
- search: parse source-keyed data ({web, images, news}) instead of a
  flat array, which threw on every live response
- use native news/images verticals and map their results
- pad the HTTP timeout over the render budget (cold scrapes exceed it)

All shapes live-verified against fastcrw.com.
- forward the search date param as a qdr tbs filter (live-verified to
  constrain results)
- default both endpoints to https://api.fastcrw.com (canonical API
  origin; live-verified for search and scrape)
- drop the jsonSchema scrape option: /v1/scrape rejects formats:json
  ('use /v1/extract'), so the option could only produce an error
Cloud returns the source groups directly under data (live-verified);
unwrap a nested results container defensively for self-host variants.
- accept the documented flat SearchResult[] data shape, routed into
  source groups by category (cloud's keyed shape still primary)
- send deadlineMs (documented, clamped 1..60000) alongside timeout
  (what the live cloud honors)
- default scrape formats to markdown+html and let the ref-marker pass
  fall back to rawHtml, so default scrapes keep Core References
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants