Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ OPENCLAW_REPO=
OPENCLAW_NODE=~/.browser-eval/toolchains/openclaw-node/node_modules/node/bin/node
OPENCLAW_PNPM=~/.browser-eval/toolchains/openclaw-node/node_modules/.bin/pnpm
DEEPSEEK_HARNESS_REPO=
# DeepSeek API (Fetch and Search): https://platform.deepseek.com/api_keys
DEEPSEEK_API_KEY=

OSS_PYTHON=.venv/bin/python
# Official Moli CLIs; each measured release binary is version- and SHA-256-gated.
Expand Down Expand Up @@ -72,7 +74,9 @@ FIRECRAWL_API_KEY=
TAVILY_API_KEY=
EXA_API_KEY=
BROWSERBASE_API_KEY=
# Optional for browser sessions; not required by the Fetch adapter.
BROWSERBASE_PROJECT_ID=
BROWSER_USE_API_KEY=
# Independent SERP comparison: https://serper.dev/ and https://brightdata.com/
SERPER_API_KEY=
BRIGHTDATA_API_KEY=
Expand All @@ -97,9 +101,10 @@ OXYLABS_PASSWORD=
# Create it in Rainbond: Personal Center -> Access Token.
RAINBOND_API_TOKEN=

# Crawl4AI Cloud: https://crawl4ai.com/dashboard/ (distinct from the local library)
CRAWL4AI_CLOUD_API_KEY=
# Web Search providers. Keep evaluation credentials in this repository file.
LEXMOUNT_SEARCH_API_KEY=
DEEPSEEK_API_KEY=
BOCHA_API_KEY=
# Optional, only for explicitly selected model relevance evaluation.
SEARCH_JUDGE_API_KEY=
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ Open the report path printed in the terminal. The example has one expected pass

Next, [try HTTP, Moli or Chromium on a local article](examples/web_fetch/README.md). Optional adapters, service credentials and live evaluation commands are covered in the track guides above.

The reusable ten-method Web Fetch cohort is selected with `--providers configured --provider-config configs/providers.example.json`; shared defaults are 30 seconds per call and one failure retry. See the [provider contract](docs/provider-contract.md#selected-native-cohort).

## Reports

Reports include comparisons, task results, full responses and failure evidence. Runs are saved under `~/.browser-eval/runs/`; use `--output /absolute/path/new-run` to choose another directory.
Expand Down
80 changes: 69 additions & 11 deletions configs/providers.example.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,61 @@
{
"schema_version": 1,
"default_providers": [
"moli-webfetch",
"firecrawl",
"cloudflare-markdown",
"tavily",
"exa",
"brightdata",
"browser-use-fetch",
"browserbase",
"deepseek-fetch-api",
"crawl4ai-cloud"
],
"providers": {
"crawl4ai-cloud": {
"type": "crawl4ai_cloud",
"api_key": "${CRAWL4AI_CLOUD_API_KEY}",
"engine": "browser",
"proxy": "residential",
"output_format": "markdown",
"dns_mode": "system",
"log_level": "debug",
"uniform_native_parameters": true
},
"deepseek-fetch-api": {
"type": "deepseek_fetch",
"api_key": "${DEEPSEEK_API_KEY}",
"dns_mode": "system",
"log_level": "debug",
"uniform_native_parameters": true
},
"brightdata": {
"type": "brightdata_unlocker",
"api_key": "${BRIGHTDATA_API_TOKEN}",
"zone": "${BRIGHTDATA_UNLOCKER_ZONE}",
"render": true,
"debug": true,
"log_level": "debug",
"dns_mode": "system",
"uniform_native_parameters": true
},
"browser-use-fetch": {
"type": "browser_use_fetch",
"api_key": "${BROWSER_USE_API_KEY}",
"proxy_country": "US",
"log_level": "debug",
"dns_mode": "system",
"uniform_native_parameters": true
},
"browserbase": {
"type": "browserbase_fetch",
"api_key": "${BROWSERBASE_API_KEY}",
"proxies": true,
"log_level": "debug",
"dns_mode": "system",
"uniform_native_parameters": true
},
"moli-webfetch": {
"type": "moli_webfetch",
"binary": "${MOLI_BINARY:-moli}",
Expand All @@ -10,14 +65,11 @@
"expected_binary_sha256": "7a53cdd071ae60a2f8ff6d08107f67f6b0f1d6247a9acd970d2bcec77ff15b50",
"wait_until": "done",
"block_private_networks": true,
"private_network_allowlist_hosts": [
"browser.lexmount.com",
"react-shopping-cart-67954.firebaseapp.com"
],
"max_bytes": 20000000,
"log_level": "debug",
"retain_complete_diagnostic_log": true,
"max_diagnostic_bytes": 100000000
"max_diagnostic_bytes": 100000000,
"uniform_native_parameters": true
},
"fixture": {
"type": "fixture",
Expand Down Expand Up @@ -53,14 +105,15 @@
},
"firecrawl": {
"type": "firecrawl",
"log_level": "debug",
"base_url": "https://api.firecrawl.dev",
"api_key": "${FIRECRAWL_API_KEY:-}",
"max_age": 0,
"max_bytes": 20000000,
"min_interval_seconds": 6.1,
"proxy": "auto",
"capacity_credits_per_call": 1,
"dynamic_wait_strategy": "actions"
"uniform_native_parameters": true
},
"firecrawl-resources": {
"type": "firecrawl",
Expand All @@ -76,11 +129,12 @@
},
"tavily": {
"type": "tavily_extract",
"log_level": "debug",
"base_url": "https://api.tavily.com",
"api_key": "${TAVILY_API_KEY}",
"extract_depth": "advanced",
"extract_timeout_seconds": 60.0,
"max_bytes": 20000000
"max_bytes": 20000000,
"uniform_native_parameters": true
},
"tavily-resources": {
"type": "tavily_extract",
Expand All @@ -93,12 +147,14 @@
},
"exa": {
"type": "exa_contents",
"log_level": "debug",
"base_url": "https://api.exa.ai",
"api_key": "${EXA_API_KEY}",
"text_verbosity": "full",
"max_age_hours": 0,
"livecrawl_timeout_ms": 15000,
"max_bytes": 20000000
"max_bytes": 20000000,
"uniform_native_parameters": true,
"rich_links_limit": 100
},
"exa-resources": {
"type": "exa_contents",
Expand All @@ -112,11 +168,13 @@
},
"cloudflare-markdown": {
"type": "cloudflare_markdown",
"log_level": "debug",
"api_token": "${CLOUDFLARE_API_TOKEN}",
"account_id": "${CLOUDFLARE_ACCOUNT_ID}",
"max_bytes": 20000000,
"document_max_bytes": 20000000,
"wait_until": "networkidle2"
"wait_until": "networkidle2",
"uniform_native_parameters": true
},
"cloudflare-content": {
"type": "cloudflare_content",
Expand Down
2 changes: 2 additions & 0 deletions docs/README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ uv run --frozen browser-harness-eval run --delay 0

接下来可以[用 HTTP、Moli 或 Chromium 读取本地文章](../examples/web_fetch/README.md)。可选适配器、服务凭据和在线评测命令见上面的各流程指南。

使用 `--providers configured --provider-config configs/providers.example.json` 选择可复用的十种 Web Fetch 方法;默认每次调用 30 秒,失败允许重试一次。详见[适配器规则](provider-contract.md#selected-native-cohort)。

## 报告

报告包含结果对比、逐题结果、完整响应和失败证据。运行结果默认保存在 `~/.browser-eval/runs/`,也可用 `--output /absolute/path/new-run` 指定新目录。
Expand Down
78 changes: 78 additions & 0 deletions docs/provider-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,42 @@ They must never be added to `metadata`, content, manifest or logs.

Use the existing scorer and report renderer.

### Hosted Markdown fetch services

Five methods share `providers/hosted_fetch.py` and the existing HTTP client,
runner and scorer. Their credentials and fixed options are declared in
`configs/providers.example.json`; credentials come from the repository `.env`.

| Method | Credentials | Native route and example options |
| --- | --- | --- |
| Bright Data | `BRIGHTDATA_API_TOKEN`, `BRIGHTDATA_UNLOCKER_ZONE` | `/request`, raw Markdown, rendering and native debug enabled |
| Browser Use Fetch | `BROWSER_USE_API_KEY` | `/fetch`, Markdown, fresh session, US proxy; no Agent |
| Browserbase Fetch | `BROWSERBASE_API_KEY` | `/v1/fetch`, Markdown, redirects and native proxy enabled; no Project ID |
| DeepSeek Fetch API | `DEEPSEEK_API_KEY` | `/v1/tools/fetch`, URL-only request; retain content and title |
| Crawl4AI Cloud | `CRAWL4AI_CLOUD_API_KEY` | `/scrape`, `format=md`, `engine=browser`, `proxy=residential` |

These adapters implement webpage body/structure tasks. Unsupported document,
media and file tasks fail before transport, without helper downloads or a
substitute browser/Agent. Rendering and internal retries remain service-owned;
offline adapter checks do not establish JavaScript capability or task pass rates.

Retain complete native responses separately from score-bearing answer fields.
An API HTTP 200 does not prove target retrieval succeeded: explicit service or
origin errors, empty content and malformed responses remain failures. API status
is distinct from target-site status. `log_level: debug` saves redacted caller
requests, response headers/body and errors; it cannot expose undisclosed service
internals. Only Bright Data receives the native debug option.

`dns_mode: system` uses the local resolver for API hosts; target-site DNS belongs
to the service. No automatic DNS fallback is added. All five services retain their
native internal timeout defaults within the caller's deadline. Historical runs
keep their recorded configuration.

API references: [Bright Data](https://docs.brightdata.com/api-reference/rest-api/unlocker/unlock-website),
[fetch-use](https://browser-use.com/posts/fetch-use),
[Browserbase Fetch](https://docs.browserbase.com/platform/fetch/overview),
[Crawl4AI Cloud](https://api.crawl4ai.com/docs/).

| Output contract | Comparison scope |
| --- | --- |
| Cleaned content: `main_text`, Markdown or text | Shared readable-content leaderboard |
Expand Down Expand Up @@ -91,3 +127,45 @@ The version-2 extraction scope describes text extraction and requires the
dynamic text route. Combining it with resource discovery is rejected before a
call. Direct file delivery also rejects DOM completion: it cannot claim a
rendered-page condition was satisfied while fetching only response bytes.

## Selected native cohort

`configs/providers.example.json` owns the ten retained methods in
`default_providers`. Use `--providers configured --provider-config
configs/providers.example.json` with `run`, `validate`, `run_benchmark.py` or
`run_certified_benchmark.py`. Selection is frozen as explicit names in evidence;
other adapters remain selectable, and the no-argument CLI still uses a fixture.

- The default is 30 seconds per physical call and one failure retry. The final
response supplies the result and primary latency; every call and accumulated
retry cost is retained. CLI overrides remain explicit and recorded.
- Stop timing after the response and required decoding; preparation for scoring,
scoring and evidence persistence follow. Discard late answers and quarantine
still-running workers rather than overlapping abandoned calls.
- Reject per-case hints and never derive requests from grading requirements.
Dynamic cases declare native retrieval mode. Exa uniformly requests 100 rich
links; Cloudflare receives URLs directly, without helper document downloads.
- Keep the 20,000,000-byte HTTP response/Moli output ceiling and debug logs.
Moli retains its pinned binary identity and private-network guard without
per-host exceptions or a caller-imposed subresource download cap. Non-public
DNS answers must be resolved as an environment issue before a public cohort.
- `crawl4ai-cloud` is distinct from local `crawl4ai`: browser versions and
extraction differ, and old local results must not be relabeled as Cloud.
Cloud uses service-cleaned Markdown; retain its engine/cache/usage fields.
Its configured native proxy adds provider charges; never switch proxy policy
in response to scores.

For diagnostics, supply frozen tasks and a new external output. `--measure-only`
retains scored evidence without building a report; it does not certify a cohort.
Historical unscored collections remain ineligible for scored reports.

```sh
uv run --frozen python scripts/run_benchmark.py \
--dataset /path/to/frozen-dataset.json \
--provider-config configs/providers.example.json --providers configured \
--timeout 30 --max-failure-retries 1 --workers 1 --measure-only \
--output /path/to/new-run
```

Publishable cohorts use `scripts/run_certified_benchmark.py` with its required
scope, work root and output. Report profiles and measured data stay outside Git.
5 changes: 4 additions & 1 deletion harness/web_fetch/artifacts.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,9 +157,12 @@ def verify_public_call_log(root: Path, binding: dict[str, Any], *, allow_public_
for event in events)
or events[0]["stage"] != "call_started"
or events[-1]["stage"] != "call_completed"
or not any(event["stage"] in {"response_received", "exception_raised"}
or not any(event["stage"] in {"response_received", "exception_raised", "answer_deadline_exceeded"}
for event in events[1:-1])):
raise ValueError("Public call log has an unexpected event schema")
if any(event["stage"] == "answer_deadline_exceeded" and event["error_type"] != "timeout"
for event in events):
raise ValueError("Deadline event must identify a timeout")
summary = payload["request"]["summary"]
if summary is not None and not isinstance(summary, dict):
raise ValueError("Public call log has an unexpected request summary")
Expand Down
8 changes: 6 additions & 2 deletions harness/web_fetch/benchmark_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from .artifacts import materialize_bound_artifacts, materialize_task_references
from .config import (
DEFAULT_CALL_TIMEOUT_SECONDS, load_dataset, load_provider_config,
load_runtime_env, add_failure_retry_argument,
load_runtime_env, add_failure_retry_argument, resolve_provider_selection,
)
from .providers import build_provider
from .reference_inputs import input_tree
Expand All @@ -28,7 +28,7 @@ def main(argv=None):
parser.add_argument('--env-file',type=Path)
parser.add_argument('--provider-config',type=Path,
help='One provider configuration for all selected tasks; defaults to the repository task-group configurations')
parser.add_argument('--providers',help='Comma-separated names; default: all benchmark methods')
parser.add_argument('--providers',help='Comma-separated names or configured; default: dataset coverage plan')
parser.add_argument('--track',help='Run only this task group, such as dynamic; default: all groups')
parser.add_argument('--case-ids',help='Comma-separated case IDs; default: every case in the selected task group')
parser.add_argument('--validate-only',action='store_true',help='Check selected configuration without creating files or calling providers')
Expand All @@ -52,6 +52,10 @@ def main(argv=None):
if args.runs < 1:parser.error('--runs must be positive')
if not args.validate_only and args.output.exists():parser.error('Output exists; choose an empty directory to avoid overwriting measured evidence')
load_runtime_env(args.env_file)
try:
args.providers = resolve_provider_selection(args.providers, args.provider_config)
except ValueError as error:
parser.error(str(error))
dataset,cases=load_dataset(args.dataset)
if args.track:
cases=[c for c in cases if c.track==args.track]
Expand Down
Loading
Loading