Skip to content

Commit 059bd3d

Browse files
chore(testing): add extended E2E benchmark session runner (#106)
1 parent 0923994 commit 059bd3d

8 files changed

Lines changed: 853 additions & 36 deletions

File tree

docs/guides/extended-testing.md

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# Extended Testing And Benchmarking (Real APIs)
2+
3+
This guide describes how to run an extended end-to-end (E2E) validation session for cascadeflow using **real provider APIs**.
4+
5+
Goals:
6+
- Validate correctness, routing logic, and cost optimization in the ways developers actually use cascadeflow:
7+
- Apps (Next.js API routes, Vercel AI SDK `useChat`)
8+
- Agents (tools, multi-turn, structured outputs)
9+
- Routers/proxies (OpenAI-compatible HTTP, existing SDKs)
10+
- Produce numbers you can share: **accuracy**, **drafter acceptance**, **cost reduction**, latency.
11+
12+
## Setup
13+
14+
1. Load provider keys (repo root `.env`):
15+
```bash
16+
set -a && source .env && set +a
17+
```
18+
19+
2. Install deps:
20+
```bash
21+
pnpm install
22+
python3 -m pip install -r requirements-dev.txt
23+
```
24+
25+
## What We Measure
26+
27+
- **Accuracy**: dataset-specific correctness (e.g. GSM8K exact match, MMLU multiple-choice, tool-call correctness).
28+
- **Drafter acceptance**: how often the cheap model is accepted without escalation.
29+
- **Cost reduction**: savings vs a verifier-only baseline.
30+
- **Latency**: end-to-end time per request (where available).
31+
32+
## Benchmark Coverage Map
33+
34+
Python benchmark suite (see `tests/benchmarks/`):
35+
- `run_benchmarks.py`: GSM8K + MMLU + MT-Bench (cost reduction + quality retention targets).
36+
- `run_all.py`: broad coverage:
37+
- HumanEval (code)
38+
- GSM8K (math)
39+
- MT-Bench (multi-turn)
40+
- TruthfulQA (factual)
41+
- Banking77 (classification)
42+
- Customer support (real-world Q&A)
43+
- BFCL agentic tool calling (multi-turn + dependencies)
44+
- Tool calling (single + multi-turn tool selection correctness)
45+
- Agentic multi-agent (router + tool call correctness)
46+
- Provider comparison (quality engine consistency across providers)
47+
48+
TypeScript coverage (monorepo tests):
49+
- `pnpm test`: builds + tests all TS packages and the Next.js `useChat` example.
50+
- Vercel AI SDK handler E2E tests:
51+
- `packages/core/src/vercel-ai/__tests__/e2e.test.ts`
52+
- `packages/core/src/__tests__/vercel-ai-chat-handler.e2e.test.ts`
53+
- Optional real API smoke:
54+
- `pnpm -C packages/core run real-api:smoke`
55+
56+
## Recommended Sessions
57+
58+
### 1) Smoke (Fast Signal, Low Spend)
59+
60+
One-command runner (writes logs/results under `benchmark_results/sessions/`):
61+
```bash
62+
set -a && source .env && set +a
63+
./scripts/extended-e2e-session.sh smoke
64+
```
65+
66+
Manual steps:
67+
```bash
68+
pnpm test
69+
python3 -m pytest
70+
71+
pnpm -C packages/core exec vitest run \
72+
src/vercel-ai/__tests__/e2e.test.ts \
73+
src/__tests__/vercel-ai-chat-handler.e2e.test.ts
74+
75+
python3 tests/benchmarks/run_benchmarks.py --quick --output benchmark_results/e2e_quick.json || true
76+
python3 -m tests.benchmarks.run_all --profile smoke --output-dir benchmark_results/smoke
77+
```
78+
79+
### 2) Standard (Shareable Numbers)
80+
81+
```bash
82+
set -a && source .env && set +a
83+
./scripts/extended-e2e-session.sh standard
84+
```
85+
86+
### 3) Overnight (Stress + Agentic)
87+
88+
```bash
89+
python3 -m tests.benchmarks.run_all --profile overnight --output-dir benchmark_results/overnight
90+
```
91+
92+
## Developer DX Validation (Out Of The Box)
93+
94+
Minimal “does it work for users tomorrow” checks:
95+
1. `docs/guides/integrate_fast.md` paths:
96+
- Vercel AI SDK `useChat` drop-in: build the example `examples/vercel-ai-nextjs/`
97+
- Proxy: validate OpenAI-compatible endpoint behavior (see `docs/guides/proxy.md`)
98+
2. Agent tools:
99+
- Tool-call generation correctness: `python3 -m tests.benchmarks.tool_calls`
100+
- Multi-turn tool history: `python3 -m tests.benchmarks.tool_calls_agentic`
101+
102+
## Notes / Current Limits
103+
104+
- Tool-call *generation* is benchmarked heavily.
105+
- Full tool *execution loops* depend on the integration path:
106+
- Streaming tool execution exists via the streaming tool manager.
107+
- Non-streaming multi-step tool execution is supported for direct routing; cascade tool paths currently focus on tool-call correctness and verification.
108+

packages/core/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"lint": "eslint src --ext .ts",
3535
"typecheck": "tsc --noEmit",
3636
"typecheck:examples": "tsc --noEmit -p examples/nodejs/tsconfig.json",
37+
"real-api:smoke": "tsx scripts/real-api-smoke.ts",
3738
"docs": "typedoc",
3839
"docs:watch": "typedoc --watch",
3940
"clean": "rm -rf dist"
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/* Real API smoke checks (developer-run).
2+
*
3+
* Runs a small set of calls against configured providers to validate:
4+
* - basic text completion
5+
* - tool-call generation + tool execution loop (direct tool loop)
6+
* - cost/acceptance fields are populated
7+
*
8+
* Usage (from repo root):
9+
* set -a && source .env && set +a
10+
* pnpm -C packages/core run real-api:smoke
11+
*/
12+
13+
import { CascadeAgent } from '../src/index.ts';
14+
import type { Tool } from '../src/types.ts';
15+
16+
type Env = Record<string, string | undefined>;
17+
18+
function requireAny(env: Env, keys: string[]): void {
19+
if (!keys.some((k) => Boolean(env[k]))) {
20+
throw new Error(`Missing API keys: expected at least one of: ${keys.join(', ')}`);
21+
}
22+
}
23+
24+
const env = process.env as Env;
25+
requireAny(env, ['OPENAI_API_KEY', 'ANTHROPIC_API_KEY']);
26+
27+
const getWeatherTool: Tool = {
28+
type: 'function',
29+
function: {
30+
name: 'get_weather',
31+
description: 'Get the weather for a location.',
32+
parameters: {
33+
type: 'object',
34+
properties: {
35+
location: { type: 'string' },
36+
},
37+
required: ['location'],
38+
},
39+
},
40+
};
41+
42+
async function main(): Promise<void> {
43+
const openaiKey = env.OPENAI_API_KEY;
44+
const anthropicKey = env.ANTHROPIC_API_KEY;
45+
46+
if (openaiKey) {
47+
const agent = new CascadeAgent({
48+
models: [
49+
{ name: 'gpt-4o-mini', provider: 'openai', cost: 0.00015, apiKey: openaiKey },
50+
{ name: 'gpt-4o', provider: 'openai', cost: 0.00625, apiKey: openaiKey },
51+
],
52+
});
53+
54+
const r1 = await agent.run('Return exactly: OK');
55+
if (!r1.content || !r1.content.includes('OK')) {
56+
throw new Error(`OpenAI smoke failed: unexpected content: ${JSON.stringify(r1.content)}`);
57+
}
58+
59+
// Tool loop: direct path only (keeps this deterministic).
60+
const r2 = await agent.run("Call get_weather for location 'Paris'. Then respond with the result.", {
61+
tools: [getWeatherTool],
62+
forceDirect: true,
63+
maxSteps: 3,
64+
toolExecutor: async (call) => {
65+
const name = call.function?.name ?? call.name;
66+
if (name !== 'get_weather') return { ok: false, error: 'unknown_tool' };
67+
return { location: 'Paris', forecast: 'sunny' };
68+
},
69+
});
70+
if (!r2.content || !/sunny/i.test(r2.content)) {
71+
throw new Error(`OpenAI tool-loop smoke failed: content=${JSON.stringify(r2.content)}`);
72+
}
73+
74+
console.log(
75+
JSON.stringify(
76+
{
77+
provider: 'openai',
78+
text: { accepted: r1.draftAccepted, model: r1.modelUsed, cost: r1.totalCost },
79+
toolLoop: { accepted: r2.draftAccepted, model: r2.modelUsed, cost: r2.totalCost },
80+
},
81+
null,
82+
2
83+
)
84+
);
85+
}
86+
87+
if (anthropicKey) {
88+
const agent = new CascadeAgent({
89+
models: [
90+
{ name: 'claude-haiku-4-5-20251001', provider: 'anthropic', cost: 0.003, apiKey: anthropicKey },
91+
{ name: 'claude-opus-4-5-20251101', provider: 'anthropic', cost: 0.045, apiKey: anthropicKey },
92+
],
93+
});
94+
95+
const r1 = await agent.run('Return exactly: OK');
96+
if (!r1.content || !r1.content.includes('OK')) {
97+
throw new Error(`Anthropic smoke failed: unexpected content: ${JSON.stringify(r1.content)}`);
98+
}
99+
100+
console.log(
101+
JSON.stringify(
102+
{
103+
provider: 'anthropic',
104+
text: { accepted: r1.draftAccepted, model: r1.modelUsed, cost: r1.totalCost },
105+
},
106+
null,
107+
2
108+
)
109+
);
110+
}
111+
}
112+
113+
main().catch((err) => {
114+
console.error(String(err?.stack ?? err));
115+
process.exit(1);
116+
});

scripts/extended-e2e-session.sh

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
# Extended E2E validation session (real APIs).
5+
#
6+
# Usage:
7+
# set -a && source .env && set +a
8+
# ./scripts/extended-e2e-session.sh smoke
9+
#
10+
# Profiles: smoke | standard | overnight | full
11+
12+
PROFILE="${1:-smoke}"
13+
TS="$(date +%Y%m%d_%H%M%S)"
14+
OUTDIR="benchmark_results/sessions/${TS}_${PROFILE}"
15+
16+
mkdir -p "${OUTDIR}"
17+
18+
{
19+
echo "timestamp=${TS}"
20+
echo "profile=${PROFILE}"
21+
echo "git_sha=$(git rev-parse HEAD)"
22+
echo "git_branch=$(git rev-parse --abbrev-ref HEAD)"
23+
echo "node=$(node -v 2>/dev/null || true)"
24+
echo "pnpm=$(pnpm -v 2>/dev/null || true)"
25+
echo "python=$(python3 -V 2>/dev/null || true)"
26+
} >"${OUTDIR}/meta.txt"
27+
28+
echo "Output: ${OUTDIR}"
29+
30+
echo
31+
echo "== TS + Python Unit/Integration Tests =="
32+
pnpm test 2>&1 | tee "${OUTDIR}/pnpm_test.log"
33+
python3 -m pytest 2>&1 | tee "${OUTDIR}/pytest.log"
34+
35+
echo
36+
echo "== Vercel AI Handler E2E (TS) =="
37+
pnpm -C packages/core exec vitest run \
38+
src/vercel-ai/__tests__/e2e.test.ts \
39+
src/__tests__/vercel-ai-chat-handler.e2e.test.ts 2>&1 | tee "${OUTDIR}/vercel_ai_e2e_vitest.log"
40+
41+
echo
42+
echo "== TS Real API Smoke (optional) =="
43+
pnpm -C packages/core run real-api:smoke 2>&1 | tee "${OUTDIR}/ts_real_api_smoke.log" || true
44+
45+
echo
46+
echo "== Python Benchmarks (triad) =="
47+
python3 tests/benchmarks/run_benchmarks.py \
48+
$([[ "${PROFILE}" == "smoke" ]] && echo "--quick" || true) \
49+
--output "${OUTDIR}/triad.json" 2>&1 | tee "${OUTDIR}/triad.log" || true
50+
51+
echo
52+
echo "== Python Benchmarks (broad suite) =="
53+
python3 -m tests.benchmarks.run_all \
54+
--profile "${PROFILE}" \
55+
--output-dir "${OUTDIR}/run_all" 2>&1 | tee "${OUTDIR}/run_all.log"
56+
57+
echo
58+
echo "Completed. Results in: ${OUTDIR}"
59+

tests/benchmarks/README.md

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ Professional benchmarks to validate CascadeFlow performance across real-world us
88
2. **Bitext Customer Support** - Customer service Q&A (27,000+ examples)
99
3. **Banking77** - Banking intent classification (13,000+ examples)
1010
4. **GSM8K** - Grade school math reasoning (8,500+ problems)
11+
5. **MT-Bench** - Multi-turn chat quality / routing behavior (sampled)
12+
6. **TruthfulQA** - Factual correctness (sampled)
13+
7. **Tool Calling** - Structured tool selection correctness (single + multi-turn)
14+
8. **BFCL Agentic** - Agentic/multi-turn tool-calling patterns (dependencies, chaining)
1115

1216
#### Metrics
1317

@@ -20,14 +24,14 @@ Each benchmark measures:
2024
#### Running Benchmarks
2125

2226
```bash
23-
# Run a single benchmark
24-
python -m benchmarks.datasets.humaneval
27+
# Load API keys (repo root .env)
28+
set -a && source .env && set +a
2529

26-
# Run all benchmarks
27-
python -m benchmarks.run_all
30+
# Run quick triad suite (GSM8K + MMLU + MT-Bench)
31+
python3 tests/benchmarks/run_benchmarks.py --quick --output benchmark_results/e2e_quick.json || true
2832

29-
# View results
30-
ls benchmarks/results/
33+
# Run broad suite (HumanEval, GSM8K, MT-Bench, TruthfulQA, Banking77, tool calling, etc.)
34+
python3 -m tests.benchmarks.run_all --profile smoke --output-dir benchmark_results/smoke
3135
```
3236

3337
#### Output
@@ -39,15 +43,19 @@ ls benchmarks/results/
3943
#### Structure
4044

4145
```
42-
benchmarks/
43-
├── base.py # Abstract benchmark class
44-
├── metrics.py # Cost/latency/quality calculations
45-
├── reporter.py # Report generation
46-
├── humaneval.py # Code generation benchmark
47-
├── customer_support.py # Customer service Q&A
48-
├── banking77.py # Banking intent classification
49-
├── gsm8k.py # Math reasoning
50-
└── results/ # Output directory
46+
tests/benchmarks/
47+
├── base.py # Benchmark base class + summary metrics
48+
├── run_benchmarks.py # GSM8K + MMLU + MT-Bench runner (targets)
49+
├── run_all.py # Broad suite runner (reports to benchmark_results/)
50+
├── humaneval/ # HumanEval benchmark implementation
51+
├── gsm8k/ # GSM8K benchmark implementation
52+
├── mmlu/ # MMLU benchmark implementation
53+
├── mtbench/ # MT-Bench benchmark implementation
54+
├── truthfulqa.py # TruthfulQA benchmark implementation
55+
├── banking77_benchmark.py
56+
├── customer_support.py
57+
├── tool_calls.py
58+
└── tool_calls_agentic.py
5159
```
5260

5361
All benchmarks extend the `Benchmark` base class.

0 commit comments

Comments
 (0)