ai-agent-book 精选快照(<2MB 代码与文档,来自 github.com/bojieli/ai-agent-book)
Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s

This commit is contained in:
2026-08-20 13:12:50 +00:00
commit b119135836
10275 changed files with 3284984 additions and 0 deletions
+427
View File
@@ -0,0 +1,427 @@
# Multi-dimensional Model Benchmarking / 多维度模型性能基准 / 实验 7-10
## English
This directory contains two layers. `demo.py` is the short interactive sampler;
`campaign.py` is the **complete, resumable Experiment 7-10 campaign**. The latter
supports OpenAI-compatible, native Anthropic, and native Gemini APIs and records
every real request in SQLite.
The complete campaign supports:
- the 8K / 32K / 128K input × 512 / 2048 output workload matrix;
- at least 100 requests per provider/model/cell;
- exact TTFT, end-to-end latency, thinking TTFT, reported reasoning length,
token usage, cache hits, and error classification;
- a 168-hour hourly availability monitor with outage duration, MTTR, and longest
continuous availability analysis;
- a measured concurrency ramp with RPM and input/output TPM saturation;
- cached/input/output pricing and a six-round Agent cost trace;
- an explicit same-model/different-provider comparison group (DeepSeek V4 Flash on the official DeepSeek API and SiliconFlow);
- resumability through unique request cells in SQLite and a strict completion audit.
The quick sampler still supports:
- **Concurrency stress testing**: sweep concurrency to identify rate limits and observe metric curves.
- **Offline mock mode** (`--mock`): synthetic data pipeline for verifying aggregation logic without API keys/network.
Synthetic mode is never accepted by `campaign.py` and cannot populate the
official evidence database.
## Full campaign
Review `campaign_config.json` before a cost-sensitive run. Pricing fields are
deliberately explicit; `analysis.py` refuses to declare the campaign complete
while any cached/input/output rate is missing.
```bash
# Real API integration smoke (small scope is visibly labelled)
python campaign.py workload --smoke --requests 1 \
--context-tokens 256 --output-tokens 128 \
--provider 'Ark/doubao-seed-1.6' \
--campaign-id integration-smoke --db results/integration-smoke.sqlite3
# Official standardized workload: defaults are 3 contexts × 2 outputs × N=100
python campaign.py workload --campaign-id release-2026-07
# Actual RPM/TPM ramp
python campaign.py rate-limit --campaign-id release-2026-07
# Six-turn stable-prefix Agent cost/cache trace
python campaign.py agent-cost --campaign-id release-2026-07
# Keep this process alive for one week; every hourly cell is resumable
python campaign.py availability --duration-hours 168 --interval-seconds 3600 \
--campaign-id release-2026-07
python analysis.py --campaign-id release-2026-07
```
`analysis.py` writes JSON and Markdown reports and prints
`Official completion: True` only after every manuscript requirement has direct
database evidence. A smoke run is useful validation, but it can never satisfy
the 100-request or 168-hour gates.
The completion audit requires every configured provider—not merely one working
provider—to have the complete workload, 169 hourly boundary probes spanning 168
hours, rate ramp, and six-round cost trace. Pricing is accepted only when input,
cached-input, and output rates are all pinned together with an authoritative
`source_url` and `as_of` date. Prices remain in the provider's published native
currency. A non-USD price also requires `usd_per_currency_unit`, `fx_source_url`,
and `fx_as_of` before the cross-provider USD cost gate can pass; CNY values are
never copied into USD-labelled fields. A null rate cannot pass simply because a
smoke run reported zero tokens in that category. The same-model provider group must also
have successful official workload cells on both endpoints. The configured
identifiers are `deepseek-v4-flash` on the official API and
`deepseek-ai/DeepSeek-V4-Flash` on SiliconFlow. Ark's
`deepseek-v4-flash-260425` remains a third independent deployment in the wider
provider matrix, but it is not substituted for either comparison arm.
## Metric definitions
| Metric | Meaning | How measured |
|---|---|---|
| Success rate | Availability | successful request count / total |
| TTFT | Time to first token | stream first non-empty chunk - request start |
| End-to-end latency | complete response time | request start -> final chunk |
| Throughput (tokens/s) | generation speed | output token count / (end-to-end - TTFT) |
| p50 / p95 / p99 | latency percentiles | interpolation over successful requests |
| std | standard deviation | per-provider latency dispersion |
| aggregate throughput / RPS | batch throughput metrics | aggregated output token rate and request rate |
If `usage.completion_tokens` is unavailable, token count falls back to chunk-count approximation with a documented caveat.
## Run
```bash
# From the repository root: use the shared Chapter 6 environment
uv sync --locked --python 3.12 --extra ch6
# Activate it before changing directories:
# macOS/Linux:
source .venv/bin/activate
# Windows PowerShell: .\.venv\Scripts\Activate.ps1
# Windows cmd: .venv\Scripts\activate.bat
# pip fallback when uv is not installed:
# python -m pip install -e ".[ch6]"
cd chapter7/model-benchmark
# Single-project compatibility path, still supported during migration:
# python -m pip install -r requirements.txt
cp env.example .env
# or export OPENAI_API_KEY=... MOONSHOT_API_KEY=... ARK_API_KEY=...
python demo.py
```
### Common parameters
```bash
python demo.py --list
python demo.py --num-requests 20 --concurrency 5
python demo.py --serial
python demo.py --max-tokens 256
```
## Specify custom endpoint/model
Use `--base-url`, `--model`, and `--api-key-env` to test a new provider without changing `DEFAULT_PROVIDERS`.
```bash
python demo.py --base-url https://api.deepseek.com --model deepseek-chat \
--api-key-env DEEPSEEK_API_KEY --name "DeepSeek官方/deepseek-chat"
```
## Concurrency sweep
```bash
python demo.py --model gpt-5.6-luna --concurrency-sweep 1,2,4,8,16 --num-requests 100
```
As concurrency increases, p95/p99/std generally get worse, and success rate may drop due to rate limits; aggregate throughput/RPS usually rises then plateaus.
## Metrics and export
```bash
python demo.py --metrics ttft,throughput
python demo.py --output result.json
```
## Offline mock validation
```bash
python demo.py --mock
python demo.py --mock --concurrency-sweep 1,2,4,8,16
```
This validates full aggregation logic with synthetic numbers labelled `[SYNTHETIC]`.
## Default providers
`DEFAULT_PROVIDERS` includes the keys that are present in environment:
- OpenAI-compatible entries (gpt-5.6-luna)
- Moonshot / doubao (explicit base_url + key)
OpenRouter fallback behavior:
- If `OPENAI_API_KEY` is missing, OpenAI-style entries can still run via OpenRouter (`OPENROUTER_API_KEY`), with model id mapping.
- For gpt-5.x, OpenRouter is preferred when `OPENROUTER_API_KEY` exists.
## Files
| File | Purpose |
|---|---|
| `benchmark.py` | core benchmark core: provider config, streaming measure, concurrency scheduling, aggregation |
| `demo.py` | CLI, parameter parsing, reporting and mock mode |
| `campaign.py` | full provider adapters, exact workloads, scheduler, rate ramp, cache trace, SQLite persistence |
| `analysis.py` | p50/p95/p99/std, outages/MTTR, RPM/TPM, cost, and completion audit |
| `campaign_config.json` | provider/model/workload/pricing inputs; no credentials |
| `test_campaign.py` | deterministic native/OpenAI adapter, persistence, and analysis tests |
| `requirements.txt` | dependencies |
| `env.example` | env templates |
## Operational boundaries
- `demo.py` retains low-cost defaults; it is not the full experiment.
- The official campaign is intentionally expensive and takes at least seven days.
- Provider pricing changes over time. Pin the rates used for a decision in
`campaign_config.json`; missing prices remain visible as an incomplete gate.
- TTFT depends heavily on geography/network.
- Offline mock is for method validation only, not production decisions.
---
## 中文
# 多维度模型性能基准测试(实验 7-10 配套代码)
对多个 OpenAI 兼容的 LLM API 提供商做横向基准测试,一条命令跑出
**TTFT / 端到端延迟 / 吞吐 / 标准差 / p50 / p95 / p99 / 成功率** 的多维度对比表,
为模型选型提供实测依据。还支持**并发压测**(逐档加压找限流点,看指标随并发的变化)
与**离线自检**`--mock` 合成数据,无需 key/网络即可验证指标聚合)。
对应《深入理解 AI Agent》第 6 章 **实验 7-10:多维度模型性能基准测试**
## 目的
本目录现在分为两层:`demo.py` 保留几分钟即可运行的低成本抽样;
`campaign.py` 则完整实现正文要求的长期实验。完整路径包含
**8K/32K/128K × 512/2048、每格至少 100 次请求、一周逐小时探测、
故障分组与 MTTR、并发爬坡实测 RPM/TPM、思考长度/延迟、缓存/输入/输出
三类价格与典型多轮 Agent 成本**。每次真实请求写入 SQLite,进程中断后可继续。
`analysis.py` 会逐项审计证据;只跑小样本或留下未填写的价格时不会误报完成。
### 完整实验命令
```bash
# 小规模真实 API 集成验证(不会被当成正式结果)
python campaign.py workload --smoke --requests 1 \
--context-tokens 256 --output-tokens 128 \
--provider 'Ark/doubao-seed-1.6' \
--campaign-id integration-smoke --db results/integration-smoke.sqlite3
# 正式负载矩阵(默认每格 N=100)
python campaign.py workload --campaign-id release-2026-07
# 逐级并发实测 RPM / TPM 上限
python campaign.py rate-limit --campaign-id release-2026-07
# 多轮 Agent 缓存与成本轨迹
python campaign.py agent-cost --campaign-id release-2026-07
# 正式一周可用性监控
python campaign.py availability --duration-hours 168 --interval-seconds 3600 \
--campaign-id release-2026-07
python analysis.py --campaign-id release-2026-07
```
正式运行前必须在 `campaign_config.json` 中固定本次决策采用的公开价格。
价格保留提供商发布的原始币种;非美元价格还必须固定带日期和来源的汇率,才能进入
跨提供商美元成本比较。程序不会把人民币数字直接写入美元字段。任何 input /
cached input / output 单价为空,完成审计都会明确失败,而不会用猜测价格填补。
## 指标定义
| 指标 | 含义 | 怎么测的 |
| --- | --- | --- |
| 成功率(可用性) | 成功请求数 / 总请求数 | 单次请求任何异常(超时/限流/网络错误/空响应)都计为失败,不中断整表 |
| TTFT | 首个 token 到达延迟 | 流式读取,记录第一个"有内容" chunk 到达的时刻 − 请求发出时刻 |
| 端到端延迟 | 请求发出到响应结束的总耗时 | 最后一个 chunk 时刻 − 请求发出时刻 |
| 吞吐(tokens/s) | 生成阶段的输出速度 | 输出 token 数 / (端到端 TTFT),剥离首 token 等待,反映纯解码速度 |
| p50 / p95 / p99 | 延迟的中位数 / 95 / 99 分位 | 对同一 (provider, model) 的多次成功请求排序后线性插值;p95、p99 高说明长尾重、体验不稳 |
| 标准差(std) | 延迟的离散程度 | 样本标准差;书中强调"高延迟方差意味着用户体验不稳定" |
| 聚合吞吐 / RPS | 整批的总吞吐 | 并发压测时:全部成功请求的输出 token 总数 / 整批墙钟耗时(RPS 为成功请求数 / 墙钟);随并发上升先增后趋平,触及服务端上限即触顶 |
> 输出 token 数优先取服务端回传的精确 `usage.completion_tokens`
> 若服务不返回 usage,则以流式 chunk 数近似计数(会略微偏高,已在代码注释标明)。
## 运行
```bash
# 在仓库根目录使用统一的第 6 章环境
uv sync --locked --python 3.12 --extra ch6
# 切换目录前先激活环境:
# macOS/Linux
source .venv/bin/activate
# Windows PowerShell.\.venv\Scripts\Activate.ps1
# Windows cmd.venv\Scripts\activate.bat
# 未安装 uv 时可用 pip 兜底:
# python -m pip install -e ".[ch6]"
cd chapter7/model-benchmark
# 迁移期间仍支持单项目兼容路径:
# python -m pip install -r requirements.txt
# 配置 key:只需填手上有的,未设置的提供商会自动跳过
cp env.example .env # 然后编辑 .env
# 或直接 export OPENAI_API_KEY=... MOONSHOT_API_KEY=... ARK_API_KEY=...
python demo.py # 一条命令跑出对比表
```
常用参数:
```bash
python demo.py --list # 仅列出将测试的提供商
python demo.py --num-requests 20 --concurrency 5 # 加大样本与并发
python demo.py --serial # 串行发送(并发=1,看无竞争下的基线延迟)
python demo.py --max-tokens 256 # 生成更长响应,更充分地测吞吐
```
默认参数(`N=10/家, 并发=3, max_tokens=64`)单次全跑成本约几分钱。
要接近书中"每配置 ≥100 次请求"的统计口径,把 `--num-requests` 调到 100 即可
(注意成本与限流会同步上升)。
### 指定任意 OpenAI 兼容端点(不改代码测新模型/新提供商)
书中要求"对同一模型测试不同 API 提供商(如 DeepSeek 官方 vs SiliconFlow"。
`--base-url / --model / --api-key-env` 即可直接指定单个端点,无需改 `DEFAULT_PROVIDERS`
```bash
python demo.py --base-url https://api.deepseek.com --model deepseek-chat \
--api-key-env DEEPSEEK_API_KEY --name "DeepSeek官方/deepseek-chat"
# 换个 base_url、保持同一 model,即可对比"同模型不同提供商"
```
### 并发压测:逐步加压找限流点
书中实验 7-10 要求"通过逐步提升并发量来找到限流点,记录 RPM/TPM 上限"。
`--concurrency-sweep` 对同一模型逐档加压,产出一张随并发变化的指标表
p50/p95/p99/std/成功率/RPS/聚合吞吐):
```bash
python demo.py --model gpt-5.6-luna --concurrency-sweep 1,2,4,8,16 --num-requests 100
```
随着并发上升,单请求延迟长尾(p95/p99/std)通常变差、可用性可能因限流下降,
而**聚合吞吐(tokens/s)与 RPS 先升后趋平**——趋平点即服务端的实际吞吐上限。
### 选择要显示的指标 / 导出结果
```bash
python demo.py --metrics ttft,throughput # 主表只看 TTFT 与吞吐(成功率始终显示)
python demo.py --output result.json # 完整结果(含 p50/p95/p99/std)写入 JSON
```
### 离线自检(`--mock`,无需 key/网络)
用**合成(synthetic)数据**跑通整条指标聚合链路,便于在没有 API key 或无网络时
验证 p50/p95/p99/std/可用性/聚合吞吐的计算是否正确。**输出数字全部为伪随机合成,
`[SYNTHETIC]` 标注,绝非真实基准,切勿用于选型。**
```bash
python demo.py --mock # 合成横向对比表
python demo.py --mock --concurrency-sweep 1,2,4,8,16 # 合成并发压测表
```
一次合成并发压测的输出(`--mock --concurrency-sweep 1,2,4,8,16 --num-requests 100`
**数字为合成,仅演示趋势**):
```
并发 | 成功率 | TTFT_p50 | TTFT_p95 | 端到端p50 | 端到端p95 | 端到端p99 | 端到端std | RPS | 聚合吞吐
-----+----------------+----------+----------+-----------+-----------+-----------+-----------+------+----------
1 | 99/100 (99%) | 301ms | 514ms | 0.73s | 1.04s | 1.16s | 0.13s | 1.3 | 49.8 t/s
2 | 100/100 (100%) | 335ms | 570ms | 0.79s | 1.07s | 1.19s | 0.15s | 2.5 | 94.4 t/s
4 | 98/100 (98%) | 381ms | 617ms | 0.83s | 1.11s | 1.19s | 0.16s | 4.7 | 180.0 t/s
8 | 92/100 (92%) | 523ms | 932ms | 0.96s | 1.53s | 1.67s | 0.25s | 8.0 | 305.3 t/s
16 | 97/100 (97%) | 878ms | 1487ms | 1.30s | 1.97s | 2.37s | 0.35s | 11.9 | 441.0 t/s
```
可见随并发上升:端到端 p95/p99 与 std 走高(长尾变差),聚合吞吐持续增长(尚未触顶)。
真实端点上这条曲线会在某个并发处趋平并伴随可用性下降——那就是限流点。
## 默认测试的提供商
代码里 `DEFAULT_PROVIDERS` 默认只跑**手上有有效 key**的提供商(OpenAI 一个 key 测多个模型):
| 展示名 | 模型 | base_url | key 环境变量 |
| --- | --- | --- | --- |
| OpenAI/gpt-5.6-luna | gpt-5.6-luna | (官方默认,可回退 OpenRouter | OPENAI_API_KEY |
| Moonshot/moonshot-v1-8k | moonshot-v1-8k | https://api.moonshot.cn/v1 | MOONSHOT_API_KEY |
| Doubao/doubao-1.5-pro-32k | doubao-1-5-pro-32k-250115 | https://ark.cn-beijing.volces.com/api/v3 | ARK_API_KEY |
> **OpenRouter 回退**`OpenAI/*` 这几条(base_url 为空的 OpenAI 原生条目)在未设置
> `OPENAI_API_KEY` 时会自动改走 **OpenRouter**`OPENROUTER_API_KEY`,模型名映射为
> `openai/*`)。`gpt-5.x` 直连 OpenAI 需组织实名认证,因此只要设置了 `OPENROUTER_API_KEY`
> 就优先走 OpenRouter。带专属 `base_url` 的条目(Kimi/豆包)不参与回退。
**提供商列表是可配置的**:在 `benchmark.py``DEFAULT_PROVIDERS` 里追加
`ProviderConfig(...)` 即可扩展。所有提供商都走同一套 OpenAI 兼容协议,
只是 `base_url``model` 不同——这正是可以"同一模型对比不同提供商"
(如书中提到的 DeepSeek 官方 vs SiliconFlow)的原因。
## 真实运行结果(示例)
以下是一次真实运行的输出(`python demo.py --num-requests 10 --concurrency 3`
测试机在中国大陆网络环境,`2026-07`)。**数字为真实测得,非虚构**
不同网络/时段会有波动,请以自己跑出的结果为准。
```
Provider/Model | 成功率 | TTFT均值 | TTFT_p95 | 端到端均值 | 端到端p95 | 吞吐 | 输出tok
--------------------------+--------------+----------+----------+------------+-----------+-----------+--------
OpenAI/gpt-5.6-luna | 10/10 (100%) | 1360ms | 2334ms | 1.73s | 2.54s | 174.9 t/s | 26
Moonshot/moonshot-v1-8k | 10/10 (100%) | 530ms | 671ms | 0.89s | 1.07s | 92.1 t/s | 32
Doubao/doubao-1.5-pro-32k | 10/10 (100%) | 1097ms | 1409ms | 2.32s | 2.91s | 36.2 t/s | 44
```
## 结论(基于上面这次运行)
- **可用性**:本次三家全部 10/10(100%)成功。可用性差异往往要在更大样本、
更高并发或更长时间窗口下才暴露——这正是书中强调"一周每小时探测"的原因。
代码已把单点失败设计成"记为可用性下降、不中断整表",便于长时间挂机采样。
- **首 token 延迟(TTFT)**:本测试机在国内网络下,Kimi 的 TTFT(~530ms)明显低于
跨境访问的 OpenAI/gpt-5.6-luna~1.36s);豆包 TTFT~1.1s)略低于 OpenAI 但端到端更长。
**TTFT 强依赖网络位置**——同一份代码在美国机房跑,OpenAI 的 TTFT 会大幅下降。
- **吞吐**:本次 gpt-5.6-luna175 t/s> Kimi92 t/s> 豆包(36 t/s)。
吞吐决定长响应的等待时间,与 TTFT 是两个独立维度。
- **稳定性(p95)**:看 p95 与均值的差距。gpt-5.6-luna 跨境访问,TTFT p95(2.33s)/均值(1.36s)
拉开较大,长尾更重;Kimi 的 p95 与均值最接近,本次最稳。
- **选型启示**:不存在"全面最优"的一家——延迟、吞吐、可用性、价格是**多维权衡**。
面向国内用户的实时交互场景,低 TTFT 的本地化服务体验更好;
批处理/长文本生成则更看重吞吐与单价。**务必在你自己的部署网络环境下实测**,
不要直接照搬第三方监测平台(如 Artificial Analysis)的数字。
## 文件说明
| 文件 | 作用 |
| --- | --- |
| `benchmark.py` | 核心:提供商配置、单次流式测量、并发调度、指标聚合(含 p99/std/聚合吞吐)、并发扫描 `sweep_concurrency`、合成数据 `synthetic_summary` |
| `demo.py` | 命令行入口:解析参数、跑测试(含并发压测 / `--mock` 离线自检)、打印对比表、导出 JSON |
| `requirements.txt` | 依赖(openai SDK + 可选 python-dotenv |
| `env.example` | key 配置模板 |
## 注意事项
- **成本控制**:默认 `max_tokens=64``N=10`,全跑成本极低。调大参数前请留意计费。
- **限流**:把并发或 N 调很大时可能触发提供商 RPM/TPM 限流,届时会以失败形式
计入可用性下降——这本身也是一种"实测限流阈值"的方式(书中实验 7-10 的一环)。
- **TTFT 与网络强相关**:跨境访问的服务 TTFT 会显著偏高,结论需结合部署地点解读。
- **OpenRouter 回退**:未设置 `OPENAI_API_KEY` 时,`OpenAI/*` 条目自动经 OpenRouter 路由
(需 `OPENROUTER_API_KEY``gpt-*` 映射为 `openai/*`);`gpt-5.x` 只要有 `OPENROUTER_API_KEY`
即优先走 OpenRouter(直连需实名认证)。其它提供商(DEEPSEEK / SILICONFLOW 等)如需启用,
`DEFAULT_PROVIDERS` 中补充配置并设置对应环境变量即可。
+864
View File
@@ -0,0 +1,864 @@
#!/usr/bin/env python3
"""Analyze the SQLite evidence produced by the full Experiment 7-10 campaign."""
from __future__ import annotations
import argparse
import json
import math
import sqlite3
import statistics
from collections import defaultdict
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Iterable, Sequence
from campaign import (
DEFAULT_CONFIG,
DEFAULT_DB,
PROMPT_SCHEMA_VERSION,
Price,
Provider,
execution_config_fingerprint,
load_config,
)
HERE = Path(__file__).resolve().parent
def percentile(values: Sequence[float], q: float) -> float | None:
if not values:
return None
ordered = sorted(values)
if len(ordered) == 1:
return ordered[0]
position = q * (len(ordered) - 1)
lower = math.floor(position)
upper = math.ceil(position)
if lower == upper:
return ordered[lower]
fraction = position - lower
return ordered[lower] + (ordered[upper] - ordered[lower]) * fraction
def describe(values: Iterable[float | int | None]) -> dict[str, float | int | None]:
present = [float(value) for value in values if value is not None]
if not present:
return {"n": 0, "mean": None, "std": None, "p50": None, "p95": None, "p99": None}
return {
"n": len(present),
"mean": statistics.fmean(present),
"std": statistics.stdev(present) if len(present) > 1 else 0.0,
"p50": percentile(present, 0.50),
"p95": percentile(present, 0.95),
"p99": percentile(present, 0.99),
}
def parse_time(value: str) -> datetime:
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
return parsed if parsed.tzinfo else parsed.replace(tzinfo=timezone.utc)
def grouped(rows: Sequence[dict[str, Any]], keys: Sequence[str]):
result: dict[tuple[Any, ...], list[dict[str, Any]]] = defaultdict(list)
for row in rows:
result[tuple(row[key] for key in keys)].append(row)
return result
def summarize_workloads(rows: Sequence[dict[str, Any]]) -> list[dict[str, Any]]:
keys = ("provider", "model", "target_context_tokens", "target_output_tokens", "concurrency")
result = []
for identity, items in sorted(grouped(rows, keys).items()):
successes = [row for row in items if row["ok"]]
wall = [row["e2e_s"] for row in successes if row["e2e_s"] is not None]
visible_output = [
int(row.get("visible_output_tokens") or max(
0, int(row.get("output_tokens") or 0) - int(row.get("reasoning_tokens") or 0)
))
for row in successes
]
generation = [
visible / max(row["e2e_s"] - row["ttft_s"], 1e-9)
for row, visible in zip(successes, visible_output)
if visible and row["e2e_s"] is not None and row["ttft_s"] is not None
]
input_prefill = [
row["input_tokens"] / max(row["ttft_s"], 1e-9)
for row in successes
if row["input_tokens"] and row["ttft_s"] is not None
]
target_output = identity[3]
result.append({
**dict(zip(keys, identity)),
"requests": len(items),
"successes": len(successes),
"success_rate": len(successes) / len(items),
"errors": dict(sorted(_counts(row["error_type"] or "unknown" for row in items if not row["ok"]).items())),
"ttft_s": describe(row["ttft_s"] for row in successes),
"e2e_s": describe(wall),
"input_prefill_throughput_tokens_s": describe(input_prefill),
"output_throughput_tokens_s": describe(generation),
"actual_input_tokens": describe(row["input_tokens"] for row in successes),
"actual_output_tokens": describe(row["output_tokens"] for row in successes),
"visible_output_tokens": describe(visible_output),
"output_length_attainment_rate": (
sum(value >= 0.95 * target_output for value in visible_output) / len(successes)
if successes else 0.0
),
"reasoning_tokens": describe(row["reasoning_tokens"] for row in successes),
"thinking_ttft_s": describe(row["thinking_ttft_s"] for row in successes),
})
return result
def _counts(values: Iterable[str]) -> dict[str, int]:
result: dict[str, int] = defaultdict(int)
for value in values:
result[value] += 1
return dict(result)
def availability_summary(rows: Sequence[dict[str, Any]]) -> list[dict[str, Any]]:
result = []
for (provider, model), items in sorted(grouped(rows, ("provider", "model")).items()):
ordered = sorted(items, key=lambda row: row["scheduled_at_utc"] or row["started_at_utc"])
outages: list[dict[str, Any]] = []
outage_start: datetime | None = None
outage_errors: list[str] = []
availability_start: datetime | None = None
continuous: list[float] = []
for row in ordered:
current = parse_time(row["scheduled_at_utc"] or row["started_at_utc"])
if row["ok"]:
if outage_start is not None:
outages.append({
"started_at_utc": outage_start.isoformat(),
"recovered_at_utc": current.isoformat(),
"duration_s": (current - outage_start).total_seconds(),
"errors": dict(_counts(outage_errors)),
"open": False,
})
outage_start = None
outage_errors = []
if availability_start is None:
availability_start = current
else:
if availability_start is not None:
continuous.append((current - availability_start).total_seconds())
availability_start = None
if outage_start is None:
outage_start = current
outage_errors.append(row["error_type"] or "unknown")
if ordered:
last = parse_time(ordered[-1]["scheduled_at_utc"] or ordered[-1]["started_at_utc"])
if availability_start is not None:
continuous.append((last - availability_start).total_seconds())
if outage_start is not None:
outages.append({
"started_at_utc": outage_start.isoformat(),
"recovered_at_utc": None,
"duration_s": (last - outage_start).total_seconds(),
"errors": dict(_counts(outage_errors)),
"open": True,
})
recovered = [outage["duration_s"] for outage in outages if not outage["open"]]
successes = sum(row["ok"] for row in ordered)
result.append({
"provider": provider,
"model": model,
"probes": len(ordered),
"successes": successes,
"uptime": successes / len(ordered) if ordered else None,
"failure_rate": 1 - successes / len(ordered) if ordered else None,
"error_types": dict(_counts(row["error_type"] or "unknown" for row in ordered if not row["ok"])),
"outages": outages,
"outage_count": len(outages),
"mttr_s": statistics.fmean(recovered) if recovered else None,
"longest_continuous_availability_s": max(continuous, default=0.0),
"observed_start_utc": (ordered[0]["scheduled_at_utc"] or ordered[0]["started_at_utc"]) if ordered else None,
"observed_end_utc": (ordered[-1]["scheduled_at_utc"] or ordered[-1]["started_at_utc"]) if ordered else None,
})
return result
def rate_limit_summary(
batches: Sequence[dict[str, Any]],
observations: Sequence[dict[str, Any]],
) -> list[dict[str, Any]]:
errors = {
identity: dict(_counts(
row["error_type"] or "unknown"
for row in items if not row["ok"]
))
for identity, items in grouped(
observations, ("provider", "model", "concurrency")
).items()
}
result = []
for row in sorted(batches, key=lambda item: (item["provider"], item["concurrency"])):
wall = row["wall_s"]
result.append({
"provider": row["provider"],
"model": row["model"],
"concurrency": row["concurrency"],
"requests": row["requested"],
"successes": row["succeeded"],
"success_rate": row["succeeded"] / row["requested"] if row["requested"] else None,
"measured_rpm": row["succeeded"] / wall * 60 if wall else None,
"measured_input_tpm": row["input_tokens"] / wall * 60 if wall else None,
"measured_output_tpm": row["output_tokens"] / wall * 60 if wall else None,
"wall_s": wall,
"errors": errors.get((row["provider"], row["model"], row["concurrency"]), {}),
})
return result
def cost_summary(
rows: Sequence[dict[str, Any]],
providers: dict[str, Provider],
) -> list[dict[str, Any]]:
result = []
for (provider_name, model, phase), items in sorted(grouped(rows, ("provider", "model", "phase")).items()):
provider = providers[provider_name]
price = provider.pricing
uncached = sum(max(0, row["input_tokens"] - row["cached_input_tokens"]) for row in items)
cached = sum(row["cached_input_tokens"] for row in items)
output = sum(row["output_tokens"] for row in items)
unpriced: dict[str, int] = {}
native_cost = 0.0
for label, tokens, rate in (
("input", uncached, price.input_per_million),
("cached_input", cached, price.cached_input_per_million),
("output", output, price.output_per_million),
):
if tokens and rate is None:
unpriced[label] = tokens
elif rate is not None:
native_cost += tokens * rate / 1_000_000
no_cache_native = None
if price.input_per_million is not None and price.output_per_million is not None:
no_cache_native = (
(uncached + cached) * price.input_per_million
+ output * price.output_per_million
) / 1_000_000
priced = not unpriced and price.native_rates_complete
usd_conversion = 1.0 if price.currency == "USD" else price.usd_per_currency_unit
comparable_usd = priced and price.usd_conversion_complete
measured_usd = native_cost * usd_conversion if comparable_usd else None
no_cache_usd = (
no_cache_native * usd_conversion
if comparable_usd and no_cache_native is not None else None
)
result.append({
"provider": provider_name,
"model": model,
"phase": phase,
"requests": len(items),
"uncached_input_tokens": uncached,
"cached_input_tokens": cached,
"output_tokens": output,
"currency": price.currency,
"measured_cost_native": native_cost if priced else None,
"no_cache_counterfactual_native": no_cache_native if priced else None,
"cache_savings_native": (
no_cache_native - native_cost
if priced and no_cache_native is not None else None
),
"usd_per_currency_unit": usd_conversion if comparable_usd else None,
"measured_cost_usd": measured_usd,
"no_cache_counterfactual_usd": no_cache_usd,
"cache_savings_usd": (
no_cache_usd - measured_usd
if no_cache_usd is not None and measured_usd is not None else None
),
"unpriced_tokens": unpriced,
"native_pricing_complete": priced,
"pricing_complete": comparable_usd,
"pricing_status": price.status,
"pricing_blocker": price.blocker,
})
return result
def external_benchmark_comparison(
config: dict[str, Any],
workload: Sequence[dict[str, Any]],
) -> list[dict[str, Any]]:
result = []
for reference in config.get("external_benchmark_references", []):
context = reference.get("context_tokens", 32768)
output = reference.get("output_tokens", 512)
measured = next((
row for row in workload
if row["provider"] == reference.get("provider")
and row["model"] == reference.get("model")
and row["target_context_tokens"] == context
and row["target_output_tokens"] == output
), None)
reference_metrics = reference.get("metrics", {})
measured_metrics = None
deltas = None
if measured:
measured_metrics = {
"ttft_p50_s": measured["ttft_s"]["p50"],
"output_throughput_p50_tokens_s": measured["output_throughput_tokens_s"]["p50"],
}
deltas = {
key: (
measured_metrics[key] - float(reference_metrics[key])
if measured_metrics.get(key) is not None and key in reference_metrics
else None
)
for key in measured_metrics
}
result.append({
"provider": reference.get("provider"),
"model": reference.get("model"),
"context_tokens": context,
"output_tokens": output,
"source_url": reference.get("source_url"),
"as_of": reference.get("as_of"),
"reference_metrics": reference_metrics,
"measured_metrics": measured_metrics,
"measured_minus_reference": deltas,
})
return result
def completion_audit(
config: dict[str, Any],
workload: Sequence[dict[str, Any]],
availability: Sequence[dict[str, Any]],
rates: Sequence[dict[str, Any]],
costs: Sequence[dict[str, Any]],
observations: Sequence[dict[str, Any]],
metadata: dict[str, Any] | None = None,
) -> dict[str, Any]:
expected_contexts = set(config["workload"]["context_tokens"])
expected_outputs = set(config["workload"]["output_tokens"])
exact_workload_design = (
expected_contexts == {8192, 32768, 131072}
and expected_outputs == {512, 2048}
and int(config["workload"].get("requests_per_cell", 0)) >= 100
)
exact_availability_design = (
float(config["availability"].get("duration_hours", 0)) >= 168
and float(config["availability"].get("interval_seconds", float("inf"))) <= 3600
and int(config["availability"].get("requests_per_probe", 0)) >= 1
)
configured_rate_levels = list(config["rate_limit"].get("concurrency_levels", []))
exact_rate_ramp_design = (
len(configured_rate_levels) >= 2
and configured_rate_levels == sorted(set(configured_rate_levels))
and int(config["rate_limit"].get("requests_per_level", 0)) >= 100
)
workload_cells = {
(row["provider"], row["target_context_tokens"], row["target_output_tokens"]): row
for row in workload
}
providers = [raw["name"] for raw in config["providers"]]
missing_cells = []
undersampled = []
unsuccessful_cells = []
for provider in providers:
for context in expected_contexts:
for output in expected_outputs:
row = workload_cells.get((provider, context, output))
if row is None:
missing_cells.append({"provider": provider, "context": context, "output": output})
elif row["requests"] < max(
100, int(config["workload"].get("requests_per_cell", 100))
):
undersampled.append({
"provider": provider, "context": context, "output": output,
"requests": row["requests"],
})
elif row["successes"] == 0:
unsuccessful_cells.append({
"provider": provider, "context": context, "output": output,
})
minimum_attainment = float(config["workload"].get("minimum_output_attainment_rate", 0.95))
output_attainment_gaps = [
{
"provider": row["provider"],
"context": row["target_context_tokens"],
"output": row["target_output_tokens"],
"attainment_rate": row.get("output_length_attainment_rate", 0.0),
"required": minimum_attainment,
}
for row in workload
if row.get("output_length_attainment_rate", 0.0) < minimum_attainment
]
availability_hours = []
availability_by_provider = {row["provider"]: row for row in availability}
for row in availability:
if row["observed_start_utc"] and row["observed_end_utc"]:
hours = (
parse_time(row["observed_end_utc"]) - parse_time(row["observed_start_utc"])
).total_seconds() / 3600
availability_hours.append({"provider": row["provider"], "hours": hours})
required_availability_hours = float(config["availability"]["duration_hours"])
expected_probe_count = int(
required_availability_hours * 3600
/ float(config["availability"]["interval_seconds"])
) + 1
missing_availability_providers = sorted(
set(providers) - set(availability_by_provider)
)
short_availability = [
row for row in availability_hours
if row["hours"] < required_availability_hours
]
undersampled_availability = [
{
"provider": provider,
"probes": availability_by_provider[provider]["probes"],
"required_probes": expected_probe_count,
}
for provider in providers
if provider in availability_by_provider
and availability_by_provider[provider]["probes"] < expected_probe_count
]
expected_levels = set(config["rate_limit"]["concurrency_levels"])
measured_levels = defaultdict(set)
undersampled_rate_levels = []
for row in rates:
measured_levels[row["provider"]].add(row["concurrency"])
if row["requests"] < config["rate_limit"]["requests_per_level"]:
undersampled_rate_levels.append({
"provider": row["provider"], "concurrency": row["concurrency"],
"requests": row["requests"],
})
missing_rate_levels = {
provider: sorted(expected_levels - measured_levels[provider])
for provider in providers if expected_levels - measured_levels[provider]
}
rate_limit_boundaries = {}
missing_rate_limit_boundaries = []
for provider in providers:
provider_rows = sorted(
(row for row in rates if row["provider"] == provider),
key=lambda row: row["concurrency"],
)
boundary = next(
(
row for row in provider_rows
if row.get("errors", {}).get("rate_limit", 0) > 0
),
None,
)
if boundary:
rate_limit_boundaries[provider] = {
"first_rate_limited_concurrency": boundary["concurrency"],
"last_pre_limit_rpm": next(
(
row["measured_rpm"] for row in reversed(provider_rows)
if row["concurrency"] < boundary["concurrency"]
and row["successes"] > 0
),
None,
),
"last_pre_limit_input_tpm": next(
(
row["measured_input_tpm"] for row in reversed(provider_rows)
if row["concurrency"] < boundary["concurrency"]
and row["successes"] > 0
),
None,
),
}
else:
missing_rate_limit_boundaries.append(provider)
cost_rounds = defaultdict(int)
for row in observations:
if row["phase"] == "agent_cost":
cost_rounds[row["provider"]] += 1
missing_cost_traces = {
provider: config["agent_cost"]["rounds"] - cost_rounds[provider]
for provider in providers if cost_rounds[provider] < config["agent_cost"]["rounds"]
}
incomplete_pricing = [
row["provider"] for row in costs
if row["phase"] == "agent_cost" and not row["pricing_complete"]
]
pricing_config_gaps = []
for raw in config["providers"]:
pricing = raw.get("pricing", {})
missing = [
field for field in (
"input_per_million",
"cached_input_per_million",
"output_per_million",
"currency",
"source_url",
"as_of",
)
if pricing.get(field) in (None, "")
]
currency = pricing.get("currency")
if currency and currency != "USD":
missing.extend(
field for field in (
"usd_per_currency_unit", "fx_source_url", "fx_as_of"
)
if pricing.get(field) in (None, "")
)
if missing:
pricing_config_gaps.append({
"provider": raw["name"],
"missing": sorted(set(missing)),
"status": pricing.get("status", "unresolved"),
"blocker": pricing.get("blocker"),
})
elif (
pricing.get("status") not in {"verified", "verified_native", "verified_with_fx"}
or not str(pricing.get("source_url", "")).startswith(("https://", "http://"))
or (currency != "USD" and not str(pricing.get("fx_source_url", "")).startswith(("https://", "http://")))
):
pricing_config_gaps.append({
"provider": raw["name"],
"missing": [],
"status": pricing.get("status", "unresolved"),
"blocker": pricing.get("blocker") or "pricing provenance/status is not validated",
})
missing_pricing_rows = [
provider for provider in providers
if not any(row["provider"] == provider and row["phase"] == "agent_cost" for row in costs)
]
thinking_providers = [
raw["name"] for raw in config["providers"]
if raw.get("thinking_budget_tokens") or any(
tag in raw["model"].casefold() for tag in ("thinking", "reason", "kimi-k3", "gpt-5")
)
]
thinking_measured = {
provider for provider in thinking_providers
if any(
row["provider"] == provider
and row["ok"]
and (row["reasoning_tokens"] > 0 or row["thinking_ttft_s"] is not None)
for row in observations
)
}
missing_thinking_metrics = sorted(set(thinking_providers) - thinking_measured)
successful_workload_rows = [
row for row in observations if row["phase"] == "workload" and row["ok"]
]
missing_raw_evidence = [
row["cell_id"] for row in successful_workload_rows
if not row.get("prompt_sha256")
or not row.get("output_sha256")
or row.get("output_text") in (None, "")
]
required_families = set(config.get("required_model_families", []))
observed_families = {
raw.get("model_family") for raw in config["providers"] if raw.get("model_family")
}
missing_model_families = sorted(required_families - observed_families)
external_reference_gaps = []
references = config.get("external_benchmark_references", [])
if not references:
external_reference_gaps.append({"reason": "no dated external benchmark reference configured"})
for reference in references:
missing = [
field for field in ("source_url", "as_of", "provider", "model", "metrics")
if not reference.get(field)
]
metrics = reference.get("metrics") or {}
missing.extend(
f"metrics.{field}"
for field in ("ttft_p50_s", "output_throughput_p50_tokens_s")
if metrics.get(field) is None
)
if reference.get("source_url") and not str(reference["source_url"]).startswith(("https://", "http://")):
missing.append("source_url_http")
if missing:
external_reference_gaps.append({
"provider": reference.get("provider"),
"model": reference.get("model"),
"missing": missing,
})
continue
matching = [
row for row in workload
if row["provider"] == reference["provider"]
and row["model"] == reference["model"]
and row["target_context_tokens"] == reference.get("context_tokens", 32768)
and row["target_output_tokens"] == reference.get("output_tokens", 512)
and row["successes"] > 0
]
if not matching:
external_reference_gaps.append({
"provider": reference["provider"],
"model": reference["model"],
"reason": "no successful matching workload cell for external comparison",
})
expected_fingerprint = execution_config_fingerprint(config)
metadata_matches = bool(
metadata
and metadata.get("execution_config_fingerprint") == expected_fingerprint
and metadata.get("prompt_schema_version") == PROMPT_SCHEMA_VERSION
)
same_model_provider_gaps = []
for group in config.get("same_model_provider_groups", []):
group_providers = group.get("providers", [])
if len(group_providers) < 2:
same_model_provider_gaps.append({
"logical_model": group.get("logical_model"),
"reason": "comparison group must name at least two providers",
})
continue
for provider in group_providers:
for context in expected_contexts:
for output in expected_outputs:
row = workload_cells.get((provider, context, output))
if (
row is None
or row["requests"] < 100
or row["successes"] == 0
):
same_model_provider_gaps.append({
"logical_model": group.get("logical_model"),
"provider": provider,
"context": context,
"output": output,
"reason": "missing successful official workload cell",
})
checks = {
"configuration_matches_exact_8k_32k_128k_x_512_2048_design": exact_workload_design,
"all_8k_32k_128k_x_512_2048_cells_present": not missing_cells,
"at_least_100_requests_per_workload_cell": not undersampled and not missing_cells,
"at_least_one_success_per_workload_cell": not unsuccessful_cells and not missing_cells,
"visible_output_length_target_attained": not output_attainment_gaps and not missing_cells,
"availability_observed_for_at_least_168_hours": (
not missing_availability_providers
and not short_availability
and not undersampled_availability
),
"availability_schedule_is_hourly_for_seven_days": exact_availability_design,
"rate_limit_ramp_configuration_is_progressive": exact_rate_ramp_design,
"all_rate_limit_levels_measured": not missing_rate_levels and not undersampled_rate_levels,
"rate_limit_boundary_identified": not missing_rate_limit_boundaries,
"multi_round_agent_cost_trace_present": not missing_cost_traces,
"cached_input_output_pricing_complete": (
not incomplete_pricing
and not missing_pricing_rows
and not pricing_config_gaps
),
"thinking_length_or_latency_measured": not missing_thinking_metrics,
"raw_request_response_evidence_present": (
bool(successful_workload_rows) and not missing_raw_evidence
),
"required_model_families_covered": bool(required_families) and not missing_model_families,
"external_monitoring_reference_compared": not external_reference_gaps,
"execution_config_fingerprint_matches": metadata_matches,
"same_model_compared_across_providers": (
bool(config.get("same_model_provider_groups"))
and not same_model_provider_gaps
),
}
return {
"official_complete": all(checks.values()),
"checks": checks,
"missing_workload_cells": missing_cells,
"undersampled_workload_cells": undersampled,
"unsuccessful_workload_cells": unsuccessful_cells,
"output_attainment_gaps": output_attainment_gaps,
"availability_hours": availability_hours,
"missing_availability_providers": missing_availability_providers,
"short_availability": short_availability,
"undersampled_availability": undersampled_availability,
"missing_rate_levels": missing_rate_levels,
"undersampled_rate_levels": undersampled_rate_levels,
"rate_limit_boundaries": rate_limit_boundaries,
"missing_rate_limit_boundaries": missing_rate_limit_boundaries,
"missing_cost_traces": missing_cost_traces,
"incomplete_pricing": incomplete_pricing,
"pricing_config_gaps": pricing_config_gaps,
"missing_pricing_rows": missing_pricing_rows,
"missing_thinking_metrics": missing_thinking_metrics,
"missing_raw_evidence_cell_ids": missing_raw_evidence,
"missing_model_families": missing_model_families,
"external_reference_gaps": external_reference_gaps,
"expected_execution_config_fingerprint": expected_fingerprint,
"observed_campaign_metadata": metadata,
"same_model_provider_gaps": same_model_provider_gaps,
}
def load_rows(connection: sqlite3.Connection, campaign_id: str, table: str) -> list[dict[str, Any]]:
rows = connection.execute(
f"SELECT * FROM {table} WHERE campaign_id = ?", (campaign_id,)
).fetchall()
return [dict(row) for row in rows]
def analyze(db: Path, config_path: Path, campaign_id: str) -> dict[str, Any]:
config = load_config(config_path)
providers = {
raw["name"]: Provider.from_dict(dict(raw)) for raw in config["providers"]
}
connection = sqlite3.connect(db)
connection.row_factory = sqlite3.Row
try:
observations = load_rows(connection, campaign_id, "observations")
batches = load_rows(connection, campaign_id, "batches")
try:
metadata_row = connection.execute(
"SELECT * FROM campaign_metadata WHERE campaign_id = ?", (campaign_id,)
).fetchone()
except sqlite3.OperationalError:
metadata_row = None
metadata = dict(metadata_row) if metadata_row else None
finally:
connection.close()
workload = summarize_workloads([row for row in observations if row["phase"] == "workload"])
availability = availability_summary([row for row in observations if row["phase"] == "availability"])
rate_observations = [row for row in observations if row["phase"] == "rate_limit"]
rates = rate_limit_summary(
[row for row in batches if row["phase"] == "rate_limit"],
rate_observations,
)
costs = cost_summary(observations, providers)
external_comparison = external_benchmark_comparison(config, workload)
return {
"schema_version": "1.0",
"campaign_id": campaign_id,
"database": str(db),
"campaign_metadata": metadata,
"observation_count": len(observations),
"workload": workload,
"availability": availability,
"rate_limits": rates,
"costs": costs,
"external_benchmark_comparison": external_comparison,
"completion_audit": completion_audit(
config, workload, availability, rates, costs, observations, metadata
),
}
def fmt(value: Any, digits: int = 3) -> str:
if value is None:
return ""
if isinstance(value, float):
return f"{value:.{digits}f}"
return str(value)
def fmt_pct(value: float | None, digits: int = 1) -> str:
if value is None:
return ""
return f"{value:.{digits}%}"
def markdown(report: dict[str, Any]) -> str:
lines = [
f"# Experiment 7-10 campaign: `{report['campaign_id']}`",
"",
f"Observations: **{report['observation_count']}**",
f"Official completion: **{report['completion_audit']['official_complete']}**",
"",
"## Standard workloads",
"",
"| Provider | Context | Output | N | Success | TTFT p50/p95/p99 (s) | E2E p50/p95/p99 (s) | Input tok/s p50 | Visible output tok/s p50 | Output attainment | Reasoning tok p50 | Thinking TTFT p50 (s) |",
"|---|---:|---:|---:|---:|---|---|---:|---:|---:|---:|---:|",
]
for row in report["workload"]:
ttft, e2e = row["ttft_s"], row["e2e_s"]
lines.append(
f"| {row['provider']} | {row['target_context_tokens']} | {row['target_output_tokens']} | "
f"{row['requests']} | {fmt_pct(row['success_rate'], 1)} | "
f"{fmt(ttft['p50'])}/{fmt(ttft['p95'])}/{fmt(ttft['p99'])} | "
f"{fmt(e2e['p50'])}/{fmt(e2e['p95'])}/{fmt(e2e['p99'])} | "
f"{fmt(row['input_prefill_throughput_tokens_s']['p50'], 1)} | "
f"{fmt(row['output_throughput_tokens_s']['p50'], 1)} | "
f"{fmt_pct(row['output_length_attainment_rate'], 1)} | "
f"{fmt(row['reasoning_tokens']['p50'], 1)} | "
f"{fmt(row['thinking_ttft_s']['p50'])} |"
)
lines.extend([
"", "## Availability", "",
"| Provider | Probes | Uptime | Outages | MTTR (s) | Longest available (h) |",
"|---|---:|---:|---:|---:|---:|",
])
for row in report["availability"]:
lines.append(
f"| {row['provider']} | {row['probes']} | {fmt_pct(row['uptime'], 2)} | "
f"{row['outage_count']} | {fmt(row['mttr_s'])} | "
f"{row['longest_continuous_availability_s'] / 3600:.2f} |"
)
lines.extend([
"", "## Measured rate-limit ramp", "",
"| Provider | Concurrency | Success | RPM | Input TPM | Output TPM |",
"|---|---:|---:|---:|---:|---:|",
])
for row in report["rate_limits"]:
lines.append(
f"| {row['provider']} | {row['concurrency']} | {fmt_pct(row['success_rate'], 1)} | "
f"{fmt(row['measured_rpm'], 1)} | {fmt(row['measured_input_tpm'], 1)} | "
f"{fmt(row['measured_output_tpm'], 1)} |"
)
lines.extend([
"", "## Native and comparable costs", "",
"| Provider | Phase | Requests | Native cost | Currency | USD cost | Cache savings (native) | Unpriced usage |",
"|---|---|---:|---:|---|---:|---:|---|",
])
for row in report["costs"]:
lines.append(
f"| {row['provider']} | {row['phase']} | {row['requests']} | "
f"{fmt(row['measured_cost_native'], 6)} | {row['currency'] or ''} | "
f"{fmt(row['measured_cost_usd'], 6)} | "
f"{fmt(row['cache_savings_native'], 6)} | "
f"{json.dumps(row['unpriced_tokens'], ensure_ascii=False)} |"
)
lines.extend(["", "## External monitoring comparison", ""])
if report["external_benchmark_comparison"]:
lines.extend(["```json", json.dumps(
report["external_benchmark_comparison"], ensure_ascii=False, indent=2
), "```"])
else:
lines.append("No dated external monitoring reference is configured; this manuscript gate is incomplete.")
lines.extend([
"", "## Completion audit", "", "```json",
json.dumps(report["completion_audit"], ensure_ascii=False, indent=2),
"```", "",
])
return "\n".join(lines)
def export_campaign_summary(
db_path: Path, config_path: Path | None = None, campaign_id: str = "experiment-7-10"
) -> dict[str, Any]:
"""Export structured JSON and Markdown summary metrics for a campaign database."""
cfg = config_path or DEFAULT_CONFIG
report = analyze(db_path, cfg, campaign_id)
md_text = markdown(report)
return {
"report": report,
"markdown": md_text,
"official_complete": report["completion_audit"]["official_complete"],
}
def main() -> int:
parser = argparse.ArgumentParser(description="Analyze full Experiment 7-10 campaign evidence")
parser.add_argument("--db", type=Path, default=DEFAULT_DB)
parser.add_argument("--config", type=Path, default=DEFAULT_CONFIG)
parser.add_argument("--campaign-id", default="experiment-7-10")
parser.add_argument("--json", type=Path, default=HERE / "results" / "campaign_report.json")
parser.add_argument("--markdown", type=Path, default=HERE / "results" / "campaign_report.md")
args = parser.parse_args()
report = analyze(args.db, args.config, args.campaign_id)
args.json.parent.mkdir(parents=True, exist_ok=True)
args.markdown.parent.mkdir(parents=True, exist_ok=True)
args.json.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
args.markdown.write_text(markdown(report), encoding="utf-8")
print(f"Wrote {args.json} and {args.markdown}")
print(f"Official completion: {report['completion_audit']['official_complete']}")
return 0 if report["completion_audit"]["official_complete"] else 2
if __name__ == "__main__":
raise SystemExit(main())
+443
View File
@@ -0,0 +1,443 @@
"""
多维度模型性能基准测试(实验 7-10 配套代码)
对多个 OpenAI 兼容的 LLM API 提供商,测量以下核心指标:
- TTFTTime To First Token,首个 token 到达延迟)
- 端到端延迟(发出请求到接收完整响应)
- 吞吐(tokens/s,按生成的输出 token 计;并发下另给聚合吞吐 / RPS)
- 标准差 / p50 / p95 / p99 延迟分位数(方差大意味着体验不稳定)
- 可用性 / 成功率(失败即计入可用性下降,不中断整表)
支持两种模式:
- 单档位对比:多提供商横向对比表(默认)。
- 并发扫描(压测):对同一模型逐步提升并发,观察延迟长尾与聚合吞吐随并发的变化。
实现要点:
- 使用 openai SDK 的流式接口(stream=True)来精确测量 TTFT。
- 通过 base_url 复用同一套 OpenAI 兼容协议,适配 Kimi / 豆包等国产 API。
- 单点请求失败被捕获并记录,不影响同一 (provider, model) 的其它请求,
也不影响其它 provider —— 这样一次运行就能测出"可用性"这一维度。
"""
from __future__ import annotations
import os
import time
import random
import statistics
from concurrent.futures import ThreadPoolExecutor, as_completed
from dataclasses import dataclass, field
from typing import Optional
from openai import OpenAI
# ---------------------------------------------------------------------------
# OpenRouter 回退:对「OpenAI 原生」条目(base_url 为空)在缺主 key 时改走 OpenRouter。
# gpt-5.x 直连 OpenAI 需组织实名认证,只要有 OPENROUTER_API_KEY 就优先走 OpenRouter。
# ---------------------------------------------------------------------------
OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1"
def _to_openrouter_model(model: str) -> str:
"""把模型名映射成 OpenRouter id:含 '/' 视为原生 idgpt-* -> openai/*
claude-* -> anthropic/claude-opus-4.8;其余回退到 openai/gpt-5.6-luna。"""
if "/" in model:
return model
if model.startswith("gpt-"):
return "openai/" + model
if model.startswith("claude-"):
return "anthropic/claude-opus-4.8"
return "openai/gpt-5.6-luna"
# ---------------------------------------------------------------------------
# 提供商配置
# ---------------------------------------------------------------------------
@dataclass
class ProviderConfig:
"""单个待测 (提供商, 模型) 配置。"""
name: str # 展示名,例如 "OpenAI/gpt-5.6-luna"
model: str # 传给 API 的模型名
api_key_env: str # 读取 API key 的环境变量名
base_url: Optional[str] = None # OpenAI 官方留空;其它填各自 base_url
def api_key(self) -> Optional[str]:
return os.environ.get(self.api_key_env)
def _openrouter_key(self) -> Optional[str]:
return os.environ.get("OPENROUTER_API_KEY", "").strip() or None
def resolve(self) -> tuple[Optional[str], Optional[str], str, bool]:
"""解析实际使用的 (api_key, base_url, model, 是否经 OpenRouter)。
仅「OpenAI 原生」条目(base_url 为空)参与回退;带专属 base_url 的条目
(如 Kimi/豆包)保持不变。回退规则:
- gpt-5.x 且有 OPENROUTER_API_KEY -> 优先走 OpenRouter(直连需实名认证);
- 否则主 key 存在 -> 直连,模型名不变;
- 否则(OpenAI 原生 + 有 OPENROUTER_API_KEY-> 走 OpenRouter,模型名映射。
"""
primary = self.api_key()
openai_native = self.base_url is None
orkey = self._openrouter_key() if openai_native else None
prefer_or = bool(orkey) and self.model.startswith("gpt-5")
if not prefer_or and primary:
return primary, self.base_url, self.model, False
if orkey:
return orkey, OPENROUTER_BASE_URL, _to_openrouter_model(self.model), True
return primary, self.base_url, self.model, False
def is_available(self) -> bool:
"""主 key 存在即可测;OpenAI 原生条目在缺主 key 时可回退 OpenRouter。"""
if self.api_key():
return True
return self.base_url is None and self._openrouter_key() is not None
# 默认只跑"手上有有效 key"的三家提供商。
# 需要扩展时,往这里追加 ProviderConfig 即可(例如 DeepSeek 官方 vs SiliconFlow 对比)。
DEFAULT_PROVIDERS: list[ProviderConfig] = [
# OpenAI 官方(一个 key 测多个模型,观察同厂不同规格的差异)
# gpt-5.6-luna 为当前廉价旗舰;无 OPENAI_API_KEY 时自动经 OpenRouter 路由
# openai/gpt-5.6-luna),gpt-5.x 只要有 OPENROUTER_API_KEY 就优先走 OpenRouter。
ProviderConfig(
name="OpenAI/gpt-5.6-luna",
model="gpt-5.6-luna",
api_key_env="OPENAI_API_KEY",
),
# 月之暗面 KimiOpenAI 兼容)
ProviderConfig(
name="Moonshot/moonshot-v1-8k",
model="moonshot-v1-8k",
api_key_env="MOONSHOT_API_KEY",
base_url="https://api.moonshot.cn/v1",
),
# 字节豆包 / 火山方舟(OpenAI 兼容)
ProviderConfig(
name="Doubao/doubao-1.5-pro-32k",
model="doubao-1-5-pro-32k-250115",
api_key_env="ARK_API_KEY",
base_url="https://ark.cn-beijing.volces.com/api/v3",
),
]
# ---------------------------------------------------------------------------
# 单次请求测量
# ---------------------------------------------------------------------------
@dataclass
class RequestResult:
"""一次流式请求的测量结果。"""
ok: bool
ttft: Optional[float] = None # 首 token 延迟(秒)
latency: Optional[float] = None # 端到端延迟(秒)
completion_tokens: Optional[int] = None # 生成的输出 token 数
throughput: Optional[float] = None # 输出吞吐(tokens/s
error: Optional[str] = None # 失败原因(可用性下降时记录)
def measure_once(
client: OpenAI,
model: str,
prompt: str,
max_tokens: int,
timeout: float,
) -> RequestResult:
"""
发起一次流式请求并测量各项指标。
任何异常都被捕获为一次"失败",用于统计可用性 —— 绝不向上抛出,
以免单点故障中断整表测试。
"""
start = time.perf_counter()
first_token_at: Optional[float] = None
completion_tokens = 0
try:
stream = client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": prompt}],
max_tokens=max_tokens,
temperature=0.0,
stream=True,
# 请求用量统计(部分 OpenAI 兼容服务支持;不支持时下方回退到计数)
stream_options={"include_usage": True},
timeout=timeout,
)
reported_tokens: Optional[int] = None
for chunk in stream:
# 首个"有内容"的 chunk 到达时刻即 TTFT
if chunk.choices:
delta = chunk.choices[0].delta
content = getattr(delta, "content", None)
if content:
if first_token_at is None:
first_token_at = time.perf_counter()
completion_tokens += 1 # 回退计数:以流式 chunk 近似 token 数
# 若服务在末尾回传了精确 usage,则以其为准
usage = getattr(chunk, "usage", None)
if usage is not None:
reported_tokens = getattr(usage, "completion_tokens", None)
end = time.perf_counter()
if first_token_at is None:
# 拿到了响应但没有任何内容 token,视为失败
return RequestResult(ok=False, error="empty response (no content token)")
final_tokens = reported_tokens if reported_tokens else completion_tokens
latency = end - start
ttft = first_token_at - start
# 吞吐按"生成阶段"计:输出 token 数 / (端到端 - 首 token 延迟)
gen_time = max(latency - ttft, 1e-6)
throughput = final_tokens / gen_time if final_tokens else 0.0
return RequestResult(
ok=True,
ttft=ttft,
latency=latency,
completion_tokens=final_tokens,
throughput=throughput,
)
except Exception as exc: # noqa: BLE001 —— 故意兜底,任何错误都记为可用性下降
return RequestResult(ok=False, error=f"{type(exc).__name__}: {exc}")
# ---------------------------------------------------------------------------
# 聚合结果
# ---------------------------------------------------------------------------
@dataclass
class ProviderSummary:
provider: str
model: str
total: int
success: int
results: list[RequestResult] = field(default_factory=list)
errors: list[str] = field(default_factory=list)
concurrency: int = 1 # 本次批次使用的并发数(并发扫描时用于标注行)
wall_time: float = 0.0 # 整批请求的墙钟耗时(秒),用于算聚合吞吐/RPS
@property
def availability(self) -> float:
return self.success / self.total if self.total else 0.0
@property
def rps(self) -> Optional[float]:
"""吞吐(请求/秒):成功请求数 / 整批墙钟耗时。并发越高一般越大,直到触顶。"""
if self.wall_time <= 0:
return None
return self.success / self.wall_time
@property
def agg_throughput(self) -> Optional[float]:
"""聚合输出吞吐(tokens/s):全部成功请求的输出 token 总数 / 整批墙钟耗时。"""
if self.wall_time <= 0:
return None
total_tokens = sum(
r.completion_tokens for r in self.results
if r.ok and r.completion_tokens
)
return total_tokens / self.wall_time if total_tokens else 0.0
def _vals(self, attr: str) -> list[float]:
return [getattr(r, attr) for r in self.results if r.ok and getattr(r, attr) is not None]
@staticmethod
def _pct(values: list[float], q: float) -> Optional[float]:
"""线性插值分位数;样本过少时退化为最大/最小值。"""
if not values:
return None
s = sorted(values)
if len(s) == 1:
return s[0]
pos = q * (len(s) - 1)
lo = int(pos)
hi = min(lo + 1, len(s) - 1)
frac = pos - lo
return s[lo] + (s[hi] - s[lo]) * frac
def stat(self, attr: str, kind: str) -> Optional[float]:
vals = self._vals(attr)
if not vals:
return None
if kind == "mean":
return statistics.mean(vals)
if kind == "std":
# 标准差:样本 <2 时无从谈起,返回 0 而非报错
return statistics.stdev(vals) if len(vals) >= 2 else 0.0
if kind == "p50":
return self._pct(vals, 0.50)
if kind == "p95":
return self._pct(vals, 0.95)
if kind == "p99":
return self._pct(vals, 0.99)
raise ValueError(kind)
def benchmark_provider(
cfg: ProviderConfig,
prompt: str,
num_requests: int,
concurrency: int,
max_tokens: int,
timeout: float,
) -> ProviderSummary:
"""对单个提供商发起 num_requests 次请求(并发 concurrency)。"""
# 这是延迟基准:显式关闭 SDK 自动重试(max_retries=0),让一次超时/挂起的
# 请求被如实记为「失败」(计入可用性下降),而不是被静默重试从而拉高延迟、
# 掩盖真实故障。每次请求仍带 per-call timeout(见 measure_once)。
# 再加一个客户端级 timeout 作为兜底,避免个别请求永久挂起拖死线程池。
# 解析实际使用的凭据/端点/模型(OpenAI 原生条目缺 key 时回退 OpenRouter)。
api_key, base_url, model, via_openrouter = cfg.resolve()
if via_openrouter:
print(f" (回退 OpenRouter{cfg.model} -> {model}", flush=True)
client = OpenAI(
api_key=api_key,
base_url=base_url,
timeout=timeout,
max_retries=0,
)
results: list[RequestResult] = []
batch_start = time.perf_counter()
if concurrency <= 1:
for _ in range(num_requests):
results.append(measure_once(client, model, prompt, max_tokens, timeout))
else:
with ThreadPoolExecutor(max_workers=concurrency) as pool:
futures = [
pool.submit(measure_once, client, model, prompt, max_tokens, timeout)
for _ in range(num_requests)
]
for fut in as_completed(futures):
results.append(fut.result())
wall_time = time.perf_counter() - batch_start
success = sum(1 for r in results if r.ok)
errors = [r.error for r in results if not r.ok and r.error]
return ProviderSummary(
provider=cfg.name,
model=model,
total=num_requests,
success=success,
results=results,
errors=errors,
concurrency=concurrency,
wall_time=wall_time,
)
def run_benchmark(
providers: list[ProviderConfig],
prompt: str,
num_requests: int,
concurrency: int,
max_tokens: int,
timeout: float,
) -> list[ProviderSummary]:
"""依次对每个提供商跑基准测试(提供商之间串行,单提供商内部并发)。"""
summaries: list[ProviderSummary] = []
for cfg in providers:
print(f" → 正在测试 {cfg.name} "
f"(model={cfg.model}, N={num_requests}, 并发={concurrency}) ...", flush=True)
summary = benchmark_provider(
cfg, prompt, num_requests, concurrency, max_tokens, timeout
)
print(f" 完成:成功 {summary.success}/{summary.total}", flush=True)
summaries.append(summary)
return summaries
def sweep_concurrency(
cfg: ProviderConfig,
prompt: str,
num_requests: int,
concurrency_levels: list[int],
max_tokens: int,
timeout: float,
) -> list[ProviderSummary]:
"""
压测:对同一 (provider, model) 逐步提升并发,返回每个并发档位的汇总。
对应书中"通过逐步提升并发量来找到限流点,记录 RPM/TPM 上限"——
随着并发上升,单请求延迟(p95)会变差、可用性可能因限流而下降,
而聚合吞吐(RPS / tokens·s⁻¹)会先升后平(触及服务端上限即触顶)。
"""
summaries: list[ProviderSummary] = []
for c in concurrency_levels:
print(f"{cfg.name} @ 并发={c} (N={num_requests}) ...", flush=True)
summary = benchmark_provider(cfg, prompt, num_requests, c, max_tokens, timeout)
print(f" 完成:成功 {summary.success}/{summary.total}, "
f"墙钟 {summary.wall_time:.2f}s", flush=True)
summaries.append(summary)
return summaries
# ---------------------------------------------------------------------------
# 合成(synthetic)数据:仅供离线演示指标聚合,绝非真实基准
# ---------------------------------------------------------------------------
def synthetic_summary(
provider: str,
model: str,
num_requests: int,
concurrency: int,
*,
base_ttft: float = 0.30,
base_gen_throughput: float = 90.0,
fail_rate: float = 0.0,
seed: int = 0,
) -> ProviderSummary:
"""
用伪随机数生成一批"看起来像真实测量"的 RequestResult,用于:
1) 在没有 API key / 没有网络时验证指标聚合数学(p50/p95/p99/std/可用性);
2) 演示并发上升时延迟长尾变差、可用性可能下降的趋势。
⚠️ 生成的所有数字都是合成的,不代表任何真实模型/提供商的性能。
并发越高,用一个简单的排队模型抬高 TTFT 与端到端延迟,仅为呈现趋势。
"""
rng = random.Random(seed + concurrency * 1000)
# 并发放大系数:并发越高,排队等待越久(简单线性 + 抖动模型)
contention = 1.0 + 0.12 * max(concurrency - 1, 0)
results: list[RequestResult] = []
total_tokens = 0
sum_latency = 0.0
for _ in range(num_requests):
# 高并发下失败率随之升高(模拟限流),封顶 60%
eff_fail = min(fail_rate * contention, 0.60)
if rng.random() < eff_fail:
results.append(RequestResult(ok=False, error="synthetic: rate_limited (429)"))
continue
# TTFT:对数正态形状,右偏(长尾),再乘并发放大
ttft = base_ttft * contention * rng.lognormvariate(0.0, 0.35)
gen_tp = max(base_gen_throughput * rng.uniform(0.75, 1.15), 1.0)
tokens = rng.randint(28, 48)
gen_time = tokens / gen_tp
latency = ttft + gen_time
total_tokens += tokens
sum_latency += latency
results.append(RequestResult(
ok=True,
ttft=ttft,
latency=latency,
completion_tokens=tokens,
throughput=gen_tp,
))
success = sum(1 for r in results if r.ok)
# 合成墙钟:把成功请求的总延迟按并发均摊,得到一个自洽的批次耗时
wall_time = max(sum_latency / max(concurrency, 1), 1e-6)
errors = [r.error for r in results if not r.ok and r.error]
return ProviderSummary(
provider=provider,
model=model,
total=num_requests,
success=success,
results=results,
errors=errors,
concurrency=concurrency,
wall_time=wall_time,
)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,242 @@
{
"providers": [
{
"name": "OpenAI/gpt-5.6-luna",
"model": "gpt-5.6-luna",
"api_key_env": "OPENAI_API_KEY",
"base_url": "https://api.openai.com/v1",
"protocol": "openai",
"model_family": "GPT",
"access_class": "closed",
"max_output_field": "max_completion_tokens",
"thinking_budget_tokens": 0,
"pricing": {
"input_per_million": 1.0,
"cached_input_per_million": 0.1,
"output_per_million": 6.0,
"currency": "USD",
"source_url": "https://openai.com/api/pricing/",
"as_of": "2026-07-29",
"status": "verified"
}
},
{
"name": "Anthropic/claude-opus-4.8",
"model": "claude-opus-4-8",
"api_key_env": "ANTHROPIC_API_KEY",
"protocol": "anthropic",
"model_family": "Claude",
"access_class": "closed",
"thinking_budget_tokens": 1024,
"pricing": {
"input_per_million": 5.0,
"cached_input_per_million": 0.5,
"output_per_million": 25.0,
"currency": "USD",
"source_url": "https://platform.claude.com/docs/en/about-claude/pricing",
"as_of": "2026-07-29",
"status": "verified"
}
},
{
"name": "Google/gemini-3.5-flash",
"model": "gemini-3.5-flash",
"api_key_env": "GEMINI_API_KEY",
"protocol": "gemini",
"model_family": "Gemini",
"access_class": "closed",
"thinking_budget_tokens": 1024,
"pricing": {
"input_per_million": 1.5,
"cached_input_per_million": 0.15,
"output_per_million": 9.0,
"currency": "USD",
"source_url": "https://ai.google.dev/gemini-api/docs/pricing",
"as_of": "2026-07-29",
"status": "verified"
}
},
{
"name": "Moonshot/kimi-k3",
"model": "kimi-k3",
"api_key_env": "MOONSHOT_API_KEY",
"base_url": "https://api.moonshot.cn/v1",
"protocol": "openai",
"model_family": "Kimi",
"access_class": "open-weight",
"thinking_budget_tokens": 1024,
"extra_body": {"reasoning_effort": "low"},
"pricing": {
"input_per_million": 20.0,
"cached_input_per_million": 2.0,
"output_per_million": 100.0,
"currency": "CNY",
"source_url": "https://platform.kimi.com/docs/pricing/chat-k3.md",
"as_of": "2026-07-29",
"usd_per_currency_unit": 0.1477922077922078,
"fx_source_url": "https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml",
"fx_as_of": "2026-07-29",
"status": "verified_with_fx"
}
},
{
"name": "Ark/doubao-seed-1.6",
"model": "doubao-seed-1-6-250615",
"api_key_env": "ARK_API_KEY",
"base_url": "https://ark.cn-beijing.volces.com/api/v3",
"protocol": "openai",
"model_family": "Doubao",
"access_class": "closed",
"thinking_budget_tokens": 0,
"extra_body": {"thinking": {"type": "disabled"}},
"pricing": {
"input_per_million": null,
"cached_input_per_million": null,
"output_per_million": null,
"currency": "CNY",
"source_url": "https://www.volcengine.com/docs/82379/1099320",
"as_of": "2026-07-29",
"status": "unresolved",
"blocker": "The exact public cached-input and output price for endpoint doubao-seed-1-6-250615 has not been extracted from an authoritative model row."
}
},
{
"name": "Ark/Qwen3-32B",
"model": "qwen3-32b-20250429",
"api_key_env": "ARK_API_KEY",
"base_url": "https://ark.cn-beijing.volces.com/api/v3",
"protocol": "openai",
"model_family": "Qwen",
"access_class": "open-weight",
"thinking_budget_tokens": 0,
"pricing": {
"input_per_million": null,
"cached_input_per_million": null,
"output_per_million": null,
"currency": "CNY",
"source_url": "https://www.volcengine.com/docs/82379/1099320",
"as_of": "2026-07-29",
"status": "unresolved",
"blocker": "The exact public price for Ark endpoint qwen3-32b-20250429 has not been matched to an authoritative model row."
}
},
{
"name": "Ark/DeepSeek-V4-Flash",
"model": "deepseek-v4-flash-260425",
"api_key_env": "ARK_API_KEY",
"base_url": "https://ark.cn-beijing.volces.com/api/v3",
"protocol": "openai",
"model_family": "DeepSeek",
"access_class": "open-weight",
"thinking_budget_tokens": 0,
"pricing": {
"input_per_million": null,
"cached_input_per_million": null,
"output_per_million": null,
"currency": "CNY",
"source_url": "https://www.volcengine.com/docs/82379/1099320",
"as_of": "2026-07-29",
"status": "unresolved",
"blocker": "The exact public price for Ark endpoint deepseek-v4-flash-260425 has not been matched to an authoritative model row."
}
},
{
"name": "DeepSeek official/DeepSeek-V4-Flash",
"model": "deepseek-v4-flash",
"api_key_env": "DEEPSEEK_API_KEY",
"base_url": "https://api.deepseek.com/v1",
"protocol": "openai",
"model_family": "DeepSeek",
"access_class": "open-weight",
"thinking_budget_tokens": 0,
"pricing": {
"input_per_million": 0.14,
"cached_input_per_million": 0.0028,
"output_per_million": 0.28,
"currency": "USD",
"source_url": "https://api-docs.deepseek.com/quick_start/pricing",
"as_of": "2026-07-29",
"status": "verified"
}
},
{
"name": "SiliconFlow/Qwen3-32B",
"model": "Qwen/Qwen3-32B",
"api_key_env": "SILICONFLOW_API_KEY",
"base_url": "https://api.siliconflow.cn/v1",
"protocol": "openai",
"model_family": "Qwen",
"access_class": "open-weight",
"thinking_budget_tokens": 0,
"pricing": {
"input_per_million": null,
"cached_input_per_million": null,
"output_per_million": null,
"currency": "CNY",
"source_url": "https://siliconflow.cn/pricing",
"as_of": "2026-07-29",
"status": "unresolved",
"blocker": "The exact Qwen/Qwen3-32B cached-input and output price has not been extracted from an authoritative public row."
}
},
{
"name": "SiliconFlow/DeepSeek-V4-Flash",
"model": "deepseek-ai/DeepSeek-V4-Flash",
"api_key_env": "SILICONFLOW_API_KEY",
"base_url": "https://api.siliconflow.cn/v1",
"protocol": "openai",
"model_family": "DeepSeek",
"access_class": "open-weight",
"thinking_budget_tokens": 0,
"pricing": {
"input_per_million": 1.0,
"cached_input_per_million": 0.02,
"output_per_million": 2.0,
"currency": "CNY",
"source_url": "https://siliconflow.cn/pricing",
"as_of": "2026-07-29",
"usd_per_currency_unit": 0.1477922077922078,
"fx_source_url": "https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml",
"fx_as_of": "2026-07-29",
"status": "verified_with_fx"
}
}
],
"required_model_families": ["GPT", "Claude", "Gemini", "Doubao", "Qwen", "Kimi", "DeepSeek"],
"external_benchmark_references": [],
"same_model_provider_groups": [
{
"logical_model": "DeepSeek-V4-Flash",
"providers": [
"DeepSeek official/DeepSeek-V4-Flash",
"SiliconFlow/DeepSeek-V4-Flash"
]
}
],
"workload": {
"context_tokens": [8192, 32768, 131072],
"output_tokens": [512, 2048],
"requests_per_cell": 100,
"concurrency": 4,
"minimum_output_attainment_rate": 0.95
},
"availability": {
"duration_hours": 168,
"interval_seconds": 3600,
"requests_per_probe": 1,
"context_tokens": 1024,
"output_tokens": 64
},
"rate_limit": {
"concurrency_levels": [1, 2, 4, 8, 16, 32],
"requests_per_level": 100,
"context_tokens": 8192,
"output_tokens": 512
},
"agent_cost": {
"rounds": 6,
"initial_context_tokens": 8192,
"tokens_added_per_round": 2048,
"output_tokens": 512
}
}
+409
View File
@@ -0,0 +1,409 @@
"""
demo.py —— 一条命令跑出多提供商性能对比表 / 并发压测表。
用法:
python demo.py # 使用默认参数,多提供商横向对比
python demo.py --num-requests 20 --concurrency 5
python demo.py --serial # 串行发送(并发=1)
python demo.py --list # 仅列出将要测试的提供商
# 指定任意一个 OpenAI 兼容端点(不改代码即可测新模型/新提供商):
python demo.py --base-url https://api.deepseek.com --model deepseek-chat \
--api-key-env DEEPSEEK_API_KEY
# 并发压测:对同一模型逐步提升并发,找限流点、看延迟长尾随并发的变化:
python demo.py --model gpt-5.6-luna --concurrency-sweep 1,2,4,8
# 离线自检(无需 key/网络):用合成数据跑通指标聚合数学
python demo.py --mock
python demo.py --mock --concurrency-sweep 1,2,4,8,16
默认只测"手上有有效 key"的提供商(OpenAI / Kimi / 豆包)。
未设置对应环境变量的提供商会被自动跳过。
"""
from __future__ import annotations
import argparse
import json
import os
# 若安装了 python-dotenv 且存在 .env,则自动加载(可选,不强制)
try:
from dotenv import load_dotenv
load_dotenv()
except Exception: # noqa: BLE001
pass
from benchmark import (
DEFAULT_PROVIDERS,
ProviderConfig,
ProviderSummary,
run_benchmark,
sweep_concurrency,
synthetic_summary,
)
# 短 prompt:控制成本,同时保证有稳定的输出用于测吞吐。
DEFAULT_PROMPT = "用一句话解释什么是大语言模型。"
# 主对比表可选的指标族(成功率始终显示)。--metrics 用逗号选择子集。
METRIC_KEYS = ["ttft", "e2e", "throughput", "tokens"]
def _fmt(v, unit: str = "", scale: float = 1.0, digits: int = 1) -> str:
"""把可能为 None 的数值格式化为对齐的字符串。"""
if v is None:
return " N/A"
return f"{v * scale:.{digits}f}{unit}"
def _render_table(headers: list[str], rows: list[list[str]]) -> None:
"""按中文宽度对齐打印一张表。"""
def width(text: str) -> int:
return sum(2 if ord(c) > 127 else 1 for c in text)
cols = len(headers)
col_w = [width(headers[i]) for i in range(cols)]
for row in rows:
for i in range(cols):
col_w[i] = max(col_w[i], width(row[i]))
def pad(text: str, w: int) -> str:
return text + " " * (w - width(text))
sep = "-+-".join("-" * col_w[i] for i in range(cols))
print()
print(" | ".join(pad(headers[i], col_w[i]) for i in range(cols)))
print(sep)
for row in rows:
print(" | ".join(pad(row[i], col_w[i]) for i in range(cols)))
print()
def _print_errors(summaries: list[ProviderSummary]) -> None:
"""打印失败明细,便于定位可用性问题。"""
if not any(s.errors for s in summaries):
return
print("失败请求明细(可用性下降原因):")
for s in summaries:
if s.errors:
for e in s.errors[:3]:
print(f" - {s.provider}: {e}")
if len(s.errors) > 3:
print(f" ... 以及另外 {len(s.errors) - 3} 条同类错误")
print()
def print_table(summaries: list[ProviderSummary], metrics: list[str]) -> None:
"""打印多提供商横向对比表(成功率 + 所选指标族)。"""
headers = ["Provider/Model", "成功率"]
for m in metrics:
if m == "ttft":
headers += ["TTFT均值", "TTFT_p95"]
elif m == "e2e":
headers += ["端到端均值", "端到端p95"]
elif m == "throughput":
headers += ["吞吐"]
elif m == "tokens":
headers += ["输出tok"]
rows: list[list[str]] = []
for s in summaries:
row = [
s.provider,
f"{s.success}/{s.total} ({s.availability * 100:.0f}%)",
]
for m in metrics:
if m == "ttft":
row += [_fmt(s.stat("ttft", "mean"), "ms", 1000, 0),
_fmt(s.stat("ttft", "p95"), "ms", 1000, 0)]
elif m == "e2e":
row += [_fmt(s.stat("latency", "mean"), "s", 1, 2),
_fmt(s.stat("latency", "p95"), "s", 1, 2)]
elif m == "throughput":
row += [_fmt(s.stat("throughput", "mean"), " t/s", 1, 1)]
elif m == "tokens":
row += [_fmt(s.stat("completion_tokens", "mean"), "", 1, 0)]
rows.append(row)
_render_table(headers, rows)
_print_errors(summaries)
def print_sweep_table(summaries: list[ProviderSummary]) -> None:
"""
打印并发压测表:每一行是一个并发档位,展示延迟长尾(p50/p95/p99/std)、
可用性与聚合吞吐(RPS / tokens·s⁻¹)随并发的变化。
"""
headers = [
"并发", "成功率", "TTFT_p50", "TTFT_p95",
"端到端p50", "端到端p95", "端到端p99", "端到端std",
"RPS", "聚合吞吐",
]
rows: list[list[str]] = []
for s in summaries:
rows.append([
str(s.concurrency),
f"{s.success}/{s.total} ({s.availability * 100:.0f}%)",
_fmt(s.stat("ttft", "p50"), "ms", 1000, 0),
_fmt(s.stat("ttft", "p95"), "ms", 1000, 0),
_fmt(s.stat("latency", "p50"), "s", 1, 2),
_fmt(s.stat("latency", "p95"), "s", 1, 2),
_fmt(s.stat("latency", "p99"), "s", 1, 2),
_fmt(s.stat("latency", "std"), "s", 1, 2),
_fmt(s.rps, "", 1, 1),
_fmt(s.agg_throughput, " t/s", 1, 1),
])
_render_table(headers, rows)
_print_errors(summaries)
def summary_to_dict(s: ProviderSummary) -> dict:
"""把一个汇总序列化为可 JSON 落盘的结构(供 --output 使用)。"""
def stats(attr: str) -> dict:
return {
k: s.stat(attr, k)
for k in ("mean", "std", "p50", "p95", "p99")
}
return {
"provider": s.provider,
"model": s.model,
"concurrency": s.concurrency,
"total": s.total,
"success": s.success,
"availability": s.availability,
"wall_time_s": s.wall_time,
"rps": s.rps,
"agg_throughput_tps": s.agg_throughput,
"ttft_s": stats("ttft"),
"latency_s": stats("latency"),
"throughput_tps": stats("throughput"),
"completion_tokens_mean": s.stat("completion_tokens", "mean"),
"errors": s.errors[:20],
}
def write_output(path: str, meta: dict, summaries: list[ProviderSummary]) -> None:
payload = {"meta": meta, "results": [summary_to_dict(s) for s in summaries]}
with open(path, "w", encoding="utf-8") as f:
json.dump(payload, f, ensure_ascii=False, indent=2)
print(f"结果已写入:{path}")
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="多维度模型性能基准测试(实验 7-10):TTFT / 端到端 / 吞吐 / p50·p95·p99·std / 可用性",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument("--num-requests", type=int, default=10,
help="每个档位的请求次数(默认 10,控制成本;书中口径 ≥100)")
parser.add_argument("--concurrency", type=int, default=3,
help="单档位并发数(默认 3;与 --concurrency-sweep 二选一)")
parser.add_argument("--serial", action="store_true",
help="串行发送(等价于 --concurrency 1,看无竞争下的基线延迟)")
parser.add_argument("--concurrency-sweep", type=str, default=None, metavar="1,2,4,8",
help="并发压测:逗号分隔的并发档位列表,对同一模型逐档加压找限流点")
parser.add_argument("--max-tokens", type=int, default=64,
help="每次请求生成的最大 token 数(默认 64,控制成本)")
parser.add_argument("--timeout", type=float, default=60.0,
help="单次请求超时(秒),超时记为可用性下降")
parser.add_argument("--prompt", type=str, default=DEFAULT_PROMPT,
help="测试用的短 prompt")
parser.add_argument("--metrics", type=str, default="all",
help="主对比表显示的指标族,逗号分隔,可选 "
"ttft/e2e/throughput/tokens 或 all(默认 all;成功率始终显示)")
parser.add_argument("--output", type=str, default=None, metavar="FILE.json",
help="把完整结果(含 p50/p95/p99/std)写入 JSON 文件")
parser.add_argument("--list", action="store_true",
help="仅列出将测试的提供商后退出")
# 指定任意单个 OpenAI 兼容端点(不改代码即可测新提供商/新模型)
grp = parser.add_argument_group("自定义端点(指定后只测这一个,忽略默认提供商列表)")
grp.add_argument("--base-url", type=str, default=None,
help="OpenAI 兼容端点的 base_urlOpenAI 官方留空)")
grp.add_argument("--model", type=str, default=None,
help="要测试的模型名(如 gpt-5.6-luna / deepseek-chat")
grp.add_argument("--api-key-env", type=str, default="OPENAI_API_KEY",
help="读取 API key 的环境变量名(默认 OPENAI_API_KEY")
grp.add_argument("--name", type=str, default=None,
help="该端点在表格中的展示名(默认用 model 名)")
parser.add_argument("--mock", action="store_true",
help="离线自检:用合成(synthetic)数据跑通指标聚合,"
"不发任何网络请求、不需要 key(数字为合成,非真实基准)")
return parser.parse_args()
def resolve_metrics(raw: str) -> list[str]:
if raw.strip().lower() == "all":
return list(METRIC_KEYS)
chosen = [m.strip() for m in raw.split(",") if m.strip()]
bad = [m for m in chosen if m not in METRIC_KEYS]
if bad:
raise SystemExit(f"未知指标:{', '.join(bad)};可选:{', '.join(METRIC_KEYS)} 或 all")
return chosen
def build_providers(args: argparse.Namespace) -> tuple[list[ProviderConfig], list[ProviderConfig]]:
"""
返回 (available, skipped)。
若指定了 --base-url 或 --model,则构造单个自定义提供商(覆盖默认列表)。
"""
if args.base_url or args.model:
if not args.model:
raise SystemExit("使用自定义端点时必须提供 --model")
cfg = ProviderConfig(
name=args.name or f"custom/{args.model}",
model=args.model,
api_key_env=args.api_key_env,
base_url=args.base_url,
)
available = [cfg] if cfg.is_available() else []
skipped = [] if cfg.is_available() else [cfg]
return available, skipped
available = [p for p in DEFAULT_PROVIDERS if p.is_available()]
skipped = [p for p in DEFAULT_PROVIDERS if not p.is_available()]
return available, skipped
def run_mock(args: argparse.Namespace, metrics: list[str]) -> None:
"""用合成数据演示指标聚合,无需 key/网络。"""
print("=" * 72)
print("多维度模型性能基准测试(实验 7-10)—— 合成数据自检模式 [SYNTHETIC]")
print("=" * 72)
print("⚠️ 以下所有数字均为合成(伪随机)生成,仅用于验证指标聚合数学,")
print(" 不代表任何真实模型/提供商/网络环境的性能,切勿作为选型依据。")
print("-" * 72)
name = args.name or (args.model and f"custom/{args.model}") or "mock/demo-model"
model = args.model or "demo-model"
if args.concurrency_sweep:
levels = parse_sweep_levels(args.concurrency_sweep)
print(f"并发压测(合成):{name} 档位={levels} N={args.num_requests}/档")
summaries = [
synthetic_summary(name, model, args.num_requests, c, fail_rate=0.02, seed=42)
for c in levels
]
print_sweep_table(summaries)
print("解读:并发上升 → 端到端 p95/p99 与 std 走高(长尾变差),")
print(" 可用性因限流下降,聚合吞吐先升后趋平(触及服务端上限即触顶)。")
else:
concurrency = 1 if args.serial else args.concurrency
print(f"单档位对比(合成):并发={concurrency} N={args.num_requests}/家")
# 造三个"提供商",参数不同以体现横向差异
summaries = [
synthetic_summary("mockA/fast-low-ttft", "fast", args.num_requests,
concurrency, base_ttft=0.20, base_gen_throughput=110, seed=1),
synthetic_summary("mockB/balanced", "balanced", args.num_requests,
concurrency, base_ttft=0.35, base_gen_throughput=85, seed=2),
synthetic_summary("mockC/high-throughput", "hi-tp", args.num_requests,
concurrency, base_ttft=0.55, base_gen_throughput=140,
fail_rate=0.05, seed=3),
]
print_table(summaries, metrics)
if args.output:
write_output(args.output, {"mode": "mock-synthetic", "note": "数字为合成,非真实基准"},
summaries)
def parse_sweep_levels(raw: str) -> list[int]:
try:
levels = [int(x) for x in raw.split(",") if x.strip()]
except ValueError:
raise SystemExit(f"--concurrency-sweep 需为逗号分隔的整数,如 1,2,4,8;收到:{raw!r}")
levels = [c for c in levels if c >= 1]
if not levels:
raise SystemExit("--concurrency-sweep 至少需要一个 ≥1 的并发档位")
return levels
def main() -> None:
args = parse_args()
metrics = resolve_metrics(args.metrics)
if args.mock:
run_mock(args, metrics)
return
available, skipped = build_providers(args)
print("=" * 72)
print("多维度模型性能基准测试(实验 7-10)")
print("=" * 72)
if skipped:
for p in skipped:
print(f"[跳过] {p.name} —— 未设置环境变量 {p.api_key_env}")
if not available:
print("没有任何可用提供商:请设置对应 API key 环境变量,")
print("或用 --mock 在无 key 情况下离线验证指标聚合。")
return
print(f"待测提供商:{', '.join(p.name for p in available)}")
# ---- 并发压测模式 ----
if args.concurrency_sweep:
levels = parse_sweep_levels(args.concurrency_sweep)
print(f"模式:并发压测(逐档加压找限流点) 档位={levels}")
print(f"参数:N={args.num_requests}/档, max_tokens={args.max_tokens}, "
f"timeout={args.timeout}s")
print(f"Prompt{args.prompt!r}")
if args.list:
return
all_summaries: list[ProviderSummary] = []
for cfg in available:
print("-" * 72)
print(f"压测 {cfg.name}:")
summaries = sweep_concurrency(
cfg, args.prompt, args.num_requests, levels,
args.max_tokens, args.timeout,
)
print_sweep_table(summaries)
all_summaries.extend(summaries)
if args.output:
write_output(args.output,
{"mode": "concurrency-sweep", "levels": levels}, all_summaries)
return
# ---- 单档位横向对比模式(默认,保持原行为)----
concurrency = 1 if args.serial else args.concurrency
print(f"参数:N={args.num_requests}/家, 并发={concurrency}, "
f"max_tokens={args.max_tokens}, timeout={args.timeout}s")
print(f"Prompt{args.prompt!r}")
if args.list:
return
print("-" * 72)
summaries = run_benchmark(
providers=available,
prompt=args.prompt,
num_requests=args.num_requests,
concurrency=concurrency,
max_tokens=args.max_tokens,
timeout=args.timeout,
)
print_table(summaries, metrics)
print("指标说明:")
print(" 成功率 = 成功请求数 / 总请求数(可用性维度)")
print(" TTFT = 首个 token 到达延迟(流式测得),越低越流畅")
print(" 端到端 = 请求发出到响应结束的总耗时")
print(" 吞吐 = 输出 token 数 / 生成阶段耗时(tokens/s)")
print(" p95 = 95 分位延迟,反映长尾/稳定性(方差大则体验不稳)")
print(" 提示 = 加 --concurrency-sweep 1,2,4,8 可做并发压测,看指标随并发的变化")
if args.output:
write_output(args.output,
{"mode": "single", "concurrency": concurrency}, summaries)
if __name__ == "__main__":
main()
+28
View File
@@ -0,0 +1,28 @@
# 复制为 .env 或直接 export 到环境变量。
# 只需填你手上有的 key,未填的提供商会被自动跳过。
# OpenAI 官方(用于 gpt-5.6-luna / kimi-k3 / doubao-1.6
OPENAI_API_KEY=your-openai-api-key
# 原生 Claude / Gemini 适配器(完整 campaign 可直接横向比较,不经中转)
ANTHROPIC_API_KEY=
GEMINI_API_KEY=
# OpenRouter 回退:未设置 OPENAI_API_KEY 时,OpenAI 原生条目自动改走 OpenRouter
# (模型名映射 gpt-* -> openai/*)。gpt-5.x 直连需组织实名认证,只要设置了本 key
# 就会优先走 OpenRouter。
# OPENROUTER_API_KEY=your-openrouter-api-key
# 月之暗面 Kimibase_url 已在代码中配置为 https://api.moonshot.cn/v1
MOONSHOT_API_KEY=your-moonshot-api-key
# 字节火山方舟 / 豆包(base_url 已配置为 https://ark.cn-beijing.volces.com/api/v3
ARK_API_KEY=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
# DeepSeek 官方与 SiliconFlow。完整实验使用同一个 DeepSeek-V4-Flash
# 权重族做“官方端点 vs 第三方端点”的受控提供商对比。
DEEPSEEK_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxx
SILICONFLOW_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxx
# 完整实验的模型、负载和三类 token 单价统一固定在 campaign_config.json。
# 不要把凭据写入该 JSON;这里只通过环境变量注入。
@@ -0,0 +1,321 @@
"""Rate Ramp Benchmark for LLM Endpoints (Chapter 6).
Simulates multi-concurrency load testing (1 to 50 req/s) against LLM endpoints,
measuring 429 rate limit backoff curves, TTFT percentiles (p50, p95, p99),
error rates, and compiling N=100 evidence packages.
"""
from __future__ import annotations
from datetime import datetime, timezone
import math
import random
from typing import Any, Callable, Dict, List, Optional, Sequence, Union
def utc_timestamp() -> str:
"""Return ISO 8601 formatted UTC timestamp."""
return datetime.now(timezone.utc).isoformat(timespec="milliseconds")
def calculate_percentile(values: Sequence[float], percentile: float) -> float:
"""Calculate percentile (0-100) using linear interpolation."""
if not values:
return 0.0
sorted_vals = sorted(values)
if len(sorted_vals) == 1:
return float(sorted_vals[0])
p = max(0.0, min(100.0, percentile))
k = (len(sorted_vals) - 1) * (p / 100.0)
f = math.floor(k)
c = math.ceil(k)
if f == c:
return float(sorted_vals[int(k)])
d0 = sorted_vals[int(f)] * (c - k)
d1 = sorted_vals[int(c)] * (k - f)
return round(float(d0 + d1), 4)
class RateRampBenchmark:
"""Simulates or executes multi-concurrency rate-ramping load tests on LLM endpoints.
Measures:
- 429 rate limit backoff curves (attempts, rate limit hits, backoff delays).
- Time To First Token (TTFT) percentiles: p50, p95, p99.
- Error rates across load levels.
- Compiles N=100 evidence packages.
"""
def __init__(self, config: Optional[dict[str, Any]] = None) -> None:
self.config = self._parse_config(config or {})
def _parse_config(self, config: dict[str, Any]) -> dict[str, Any]:
start_rate = int(config.get("start_rate", 1))
end_rate = int(config.get("end_rate", 50))
step_rate = int(config.get("step_rate", 5))
if "rates" in config and isinstance(config["rates"], (list, tuple)) and config["rates"]:
rates = [int(r) for r in config["rates"]]
start_rate = rates[0]
end_rate = rates[-1]
else:
if start_rate <= end_rate:
step = max(1, step_rate)
rates = list(range(start_rate, end_rate + 1, step))
else:
step = -max(1, abs(step_rate))
rates = list(range(start_rate, end_rate - 1, step))
if not rates or rates[-1] != end_rate:
rates.append(end_rate)
return {
"start_rate": start_rate,
"end_rate": end_rate,
"step_rate": step_rate,
"rates": rates,
"requests_per_step": int(config.get("requests_per_step", 15)),
"sample_size": int(config.get("sample_size", 100)),
"endpoint_url": str(config.get("endpoint_url", "https://api.openai.com/v1/chat/completions")),
"model": str(config.get("model", "gpt-4o")),
"max_backoff_sec": float(config.get("max_backoff_sec", 8.0)),
"request_fn": config.get("request_fn"),
}
def simulate_request(
self, target_rate: int, concurrency: int, request_idx: int
) -> dict[str, Any]:
"""Simulate a single endpoint request under load when no live request_fn is provided."""
# Seed deterministically for test consistency
rng = random.Random(target_rate * 1000 + request_idx)
# Base TTFT increases slightly with rate/concurrency
base_ttft = 0.05 + (target_rate / 100.0) * 0.35 + rng.uniform(0.01, 0.05)
ttft_sec = round(base_ttft, 4)
# 429 probability ramps up as target_rate exceeds 25 req/s
prob_429 = max(0.0, (target_rate - 20) / 40.0) if target_rate > 20 else 0.0
is_429 = rng.random() < prob_429
prob_5xx = 0.03 if target_rate > 40 else 0.01
is_5xx = not is_429 and (rng.random() < prob_5xx)
if is_429:
status_code = 429
retry_count = rng.randint(1, 3)
backoff_sec = round(min(self.config["max_backoff_sec"], 0.4 * (2 ** (retry_count - 1)) + rng.uniform(0.05, 0.2)), 4)
error_type = "rate_limit_429"
elif is_5xx:
status_code = 500
retry_count = 0
backoff_sec = 0.0
error_type = "server_error_500"
else:
status_code = 200
retry_count = 0
backoff_sec = 0.0
error_type = None
total_latency_sec = round(ttft_sec + rng.uniform(0.1, 0.3) + backoff_sec, 4)
return {
"request_id": f"req-{target_rate:02d}-{request_idx:03d}",
"timestamp": utc_timestamp(),
"target_rate": target_rate,
"concurrency": concurrency,
"status_code": status_code,
"ttft_sec": ttft_sec,
"total_latency_sec": total_latency_sec,
"backoff_sec": backoff_sec,
"retry_count": retry_count,
"error_type": error_type,
}
def compile_evidence_package(
self, records: Sequence[dict[str, Any]], sample_size: int = 100
) -> list[dict[str, Any]]:
"""Compile exactly sample_size (default N=100) evidence items uniformly sampled from raw records."""
if not records or not sample_size or sample_size <= 0:
return []
valid_records = [r for r in records if isinstance(r, dict)]
if not valid_records:
return []
if len(valid_records) <= sample_size:
return [dict(r) for r in valid_records]
# Uniformly sample across the records to cover all rate tiers
step = len(valid_records) / float(sample_size)
indices = [int(i * step) for i in range(sample_size)]
return [dict(valid_records[idx]) for idx in indices]
def calculate_backoff_curves(
self, records: Sequence[dict[str, Any]]
) -> dict[str, Any]:
"""Compute 429 rate limit backoff curve metrics by request rate level."""
by_rate: dict[int, dict[str, Any]] = {}
total_429_backoff_time = 0.0
max_backoff = 0.0
total_429 = 0
total_429_backoff_count = 0
grouped: dict[int, list[dict[str, Any]]] = {}
for r in records:
if not isinstance(r, dict):
continue
rate = int(r.get("target_rate", 0) or 0)
grouped.setdefault(rate, []).append(r)
for rate in sorted(grouped.keys()):
step_recs = grouped[rate]
cnt = len(step_recs)
hits_429 = sum(1 for r in step_recs if isinstance(r, dict) and r.get("status_code") == 429)
backoffs_429 = [
float(r.get("backoff_sec") or 0.0)
for r in step_recs
if isinstance(r, dict)
and r.get("status_code") == 429
and float(r.get("backoff_sec") or 0.0) > 0
]
avg_backoff = round(sum(backoffs_429) / len(backoffs_429), 4) if backoffs_429 else 0.0
step_max_backoff = max(backoffs_429, default=0.0)
total_429 += hits_429
total_429_backoff_count += len(backoffs_429)
total_429_backoff_time += sum(backoffs_429)
max_backoff = max(max_backoff, step_max_backoff)
by_rate[rate] = {
"total_requests": cnt,
"429_count": hits_429,
"backoff_ratio": round(hits_429 / cnt, 4) if cnt > 0 else 0.0,
"avg_backoff_sec": avg_backoff,
"max_backoff_sec": round(step_max_backoff, 4),
}
overall_avg_backoff = (
round(total_429_backoff_time / total_429_backoff_count, 4)
if total_429_backoff_count > 0
else 0.0
)
return {
"by_rate": by_rate,
"overall_avg_backoff_sec": overall_avg_backoff,
"total_backoff_time_sec": round(total_429_backoff_time, 4),
"max_backoff_observed_sec": round(max_backoff, 4),
"total_429_count": total_429,
}
def run(self, config: Optional[dict[str, Any]] = None) -> dict[str, Any]:
"""Execute rate ramp benchmark and return structured benchmark metrics."""
if config is not None:
self.config = self._parse_config(config)
rates = self.config["rates"]
reqs_per_step = self.config["requests_per_step"]
request_fn: Optional[Callable] = self.config["request_fn"]
all_records: list[dict[str, Any]] = []
ramp_steps_summary: list[dict[str, Any]] = []
for rate in rates:
concurrency = rate
step_records: list[dict[str, Any]] = []
for i in range(reqs_per_step):
if callable(request_fn):
rec = request_fn(rate, concurrency, i)
else:
rec = self.simulate_request(rate, concurrency, i)
step_records.append(rec)
all_records.append(rec)
ttfts = [float(r["ttft_sec"]) for r in step_records if isinstance(r, dict) and r.get("ttft_sec") is not None]
hits_429 = sum(1 for r in step_records if isinstance(r, dict) and r.get("status_code") == 429)
other_errs = sum(
1
for r in step_records
if not isinstance(r, dict)
or r.get("status_code") not in (200, 429)
)
successes = sum(1 for r in step_records if isinstance(r, dict) and r.get("status_code") == 200)
# Only throttled (429) requests contribute backoff time to averages.
backoff_secs = [
float(r.get("backoff_sec", 0.0) or 0.0)
for r in step_records
if isinstance(r, dict)
and r.get("status_code") == 429
and float(r.get("backoff_sec", 0.0) or 0.0) > 0
]
avg_backoff = (
round(sum(backoff_secs) / len(backoff_secs), 4)
if backoff_secs
else 0.0
)
ramp_steps_summary.append(
{
"rate_req_per_sec": rate,
"concurrency": concurrency,
"total_requests": len(step_records),
"successful_requests": successes,
"rate_limit_errors": hits_429,
"other_errors": other_errs,
"error_rate": round((hits_429 + other_errs) / max(1, len(step_records)), 4),
"ttft_p50": calculate_percentile(ttfts, 50),
"ttft_p95": calculate_percentile(ttfts, 95),
"ttft_p99": calculate_percentile(ttfts, 99),
"avg_backoff_sec": avg_backoff,
}
)
all_ttfts = [float(r.get("ttft_sec", 0.0) or 0.0) for r in all_records if isinstance(r, dict)]
total_reqs = len(all_records)
total_429 = sum(1 for r in all_records if isinstance(r, dict) and r.get("status_code") == 429)
total_other = sum(
1 for r in all_records if isinstance(r, dict) and r.get("status_code") not in (200, 429)
)
total_success = sum(1 for r in all_records if isinstance(r, dict) and r.get("status_code") == 200)
backoff_curves = self.calculate_backoff_curves(all_records)
evidence_package = self.compile_evidence_package(
all_records, sample_size=self.config["sample_size"]
)
overall_metrics = {
"total_requests": total_reqs,
"successful_requests": total_success,
"total_errors": total_429 + total_other,
"error_rate": round((total_429 + total_other) / max(1, total_reqs), 4),
"rate_limit_429_count": total_429,
"ttft_p50": calculate_percentile(all_ttfts, 50),
"ttft_p95": calculate_percentile(all_ttfts, 95),
"ttft_p99": calculate_percentile(all_ttfts, 99),
"avg_backoff_sec": backoff_curves["overall_avg_backoff_sec"],
}
return {
"config": {
"start_rate": self.config["start_rate"],
"end_rate": self.config["end_rate"],
"step_rate": self.config["step_rate"],
"sample_size": self.config["sample_size"],
"endpoint_url": self.config["endpoint_url"],
"model": self.config["model"],
},
"ramp_steps": ramp_steps_summary,
"overall_metrics": overall_metrics,
"backoff_curves": backoff_curves,
"evidence_package": evidence_package,
}
def run_benchmark(config: Optional[dict[str, Any]] = None) -> dict[str, Any]:
"""Entrypoint function to run rate ramp benchmark and return structured metrics."""
bench = RateRampBenchmark(config)
return bench.run()
@@ -0,0 +1,7 @@
openai>=1.30.0
anthropic>=0.59.0
google-genai>=1.30.0
tiktoken>=0.7.0
pytest>=8.0.0
# 可选:自动加载 .env 文件(demo.py 里做了软依赖,未安装也能跑)
python-dotenv>=1.0.0
+425
View File
@@ -0,0 +1,425 @@
from __future__ import annotations
from pathlib import Path
from types import SimpleNamespace
from analysis import availability_summary, completion_audit, markdown, percentile, summarize_workloads
from campaign import (
CampaignStore,
Observation,
Price,
PromptFactory,
Provider,
error_details,
execution_config_fingerprint,
measure_anthropic,
measure_gemini,
measure_stream,
)
class FakeCompletions:
def create(self, **kwargs):
assert kwargs["stream"] is True
usage = SimpleNamespace(
prompt_tokens=100,
completion_tokens=12,
prompt_tokens_details=SimpleNamespace(cached_tokens=40),
completion_tokens_details=SimpleNamespace(reasoning_tokens=5),
)
return iter([
SimpleNamespace(
id="request-1", usage=None,
choices=[SimpleNamespace(
finish_reason=None,
delta=SimpleNamespace(content=None, reasoning_content="think"),
)],
),
SimpleNamespace(
id="request-1", usage=None,
choices=[SimpleNamespace(
finish_reason=None,
delta=SimpleNamespace(content="answer", reasoning_content=None),
)],
),
SimpleNamespace(id="request-1", usage=usage, choices=[]),
])
def fake_client():
return SimpleNamespace(chat=SimpleNamespace(completions=FakeCompletions()))
def provider():
return Provider(
name="test", model="test-model", api_key_env="UNUSED", base_url="https://example.test/v1",
pricing=Price(1.0, 0.1, 2.0, currency="USD", source_url="https://example.test", as_of="2026-07-29", status="verified"),
)
def observation(cell: str, ok: bool, scheduled: str, **overrides):
values = dict(
campaign_id="campaign", phase="availability", cell_id=cell,
provider="test", model="test-model", scheduled_at_utc=scheduled,
started_at_utc=scheduled, ended_at_utc=scheduled,
target_context_tokens=100, target_output_tokens=10, concurrency=1,
request_index=0, ok=ok,
)
values.update(overrides)
return Observation(**values)
def test_prompt_factory_hits_exact_reference_token_count():
factory = PromptFactory()
text = factory.build(256, 64)
assert len(factory.encoding.encode(text)) == 256
assert "64 tokens" in text
def test_measure_stream_records_usage_cache_reasoning_and_hash():
row = measure_stream(
provider(), campaign_id="c", phase="workload", cell_id="id", prompt="hello",
target_context_tokens=10, target_output_tokens=12, concurrency=1,
request_index=0, client=fake_client(),
)
assert row.ok is True
assert row.input_tokens == 100
assert row.cached_input_tokens == 40
assert row.output_tokens == 12
assert row.visible_output_tokens == 7
assert row.reasoning_tokens == 5
assert row.thinking_ttft_s is not None
assert row.output_sha256
assert row.output_text == "answer"
assert row.prompt_sha256
def test_store_is_resumable_and_ignores_duplicate_cell(tmp_path: Path):
store = CampaignStore(tmp_path / "campaign.sqlite3")
row = observation("same", True, "2026-01-01T00:00:00+00:00")
store.add(row)
store.add(row)
assert store.has("same")
count = store.connection.execute("SELECT count(*) FROM observations").fetchone()[0]
store.close()
assert count == 1
def test_campaign_binding_rejects_changed_execution_but_allows_repricing(tmp_path: Path):
import pytest
config = {
"providers": [{
"name": "p", "model": "m", "api_key_env": "KEY",
"protocol": "openai", "pricing": {"input_per_million": 1.0},
}],
"workload": {"context_tokens": [8192], "output_tokens": [512], "requests_per_cell": 100},
"availability": {"duration_hours": 168, "interval_seconds": 3600},
"rate_limit": {"concurrency_levels": [1, 2], "requests_per_level": 100},
"agent_cost": {"rounds": 2},
}
store = CampaignStore(tmp_path / "campaign.sqlite3")
first = store.bind_campaign("c", config)
repriced = {**config, "providers": [{
**config["providers"][0], "pricing": {"input_per_million": 2.0},
}]}
assert store.bind_campaign("c", repriced) == first
changed = {**config, "workload": {**config["workload"], "output_tokens": [2048]}}
assert execution_config_fingerprint(changed) != first
with pytest.raises(RuntimeError, match="bound to execution fingerprint"):
store.bind_campaign("c", changed)
store.close()
def test_batch_counts_accumulate_across_resumed_invocations(tmp_path: Path):
store = CampaignStore(tmp_path / "campaign.sqlite3")
payload = {
"batch_id": "b", "campaign_id": "c", "phase": "workload",
"provider": "p", "model": "m", "target_context_tokens": 8192,
"target_output_tokens": 512, "concurrency": 4,
"requested": 1, "succeeded": 1, "input_tokens": 8192,
"output_tokens": 512, "wall_s": 1.0,
"started_at_utc": "2026-01-01T00:00:00+00:00",
"ended_at_utc": "2026-01-01T00:00:01+00:00",
}
store.add_batch(payload)
store.add_batch({
**payload, "requested": 99, "succeeded": 98,
"input_tokens": 99 * 8192, "output_tokens": 98 * 512,
"wall_s": 10.0,
})
row = store.connection.execute(
"SELECT requested, succeeded, input_tokens, output_tokens, wall_s FROM batches"
).fetchone()
assert tuple(row) == (100, 99, 100 * 8192, 99 * 512, 11.0)
store.close()
def test_availability_groups_failures_and_computes_mttr():
rows = [
observation("1", True, "2026-01-01T00:00:00+00:00"),
observation("2", False, "2026-01-01T01:00:00+00:00", error_type="provider_5xx"),
observation("3", False, "2026-01-01T02:00:00+00:00", error_type="provider_5xx"),
observation("4", True, "2026-01-01T03:00:00+00:00"),
]
summary = availability_summary([row.__dict__ | {"ok": int(row.ok)} for row in rows])[0]
assert summary["uptime"] == 0.5
assert summary["outage_count"] == 1
assert summary["mttr_s"] == 7200
def test_percentile_and_workload_summary_include_output_attainment():
assert percentile([1, 2, 3], 0.5) == 2
rows = [
observation(
"1", True, "2026-01-01T00:00:00+00:00", phase="workload",
target_output_tokens=100, input_tokens=200, output_tokens=100,
ttft_s=0.1, e2e_s=1.1,
).__dict__,
observation(
"2", True, "2026-01-01T00:01:00+00:00", phase="workload",
target_output_tokens=100, input_tokens=200, output_tokens=90,
ttft_s=0.2, e2e_s=1.2,
).__dict__,
]
summary = summarize_workloads(rows)[0]
assert summary["requests"] == 2
assert summary["output_length_attainment_rate"] == 0.5
def test_error_classification_detects_rate_limit():
exc = RuntimeError("429 rate limit exceeded")
_, category, _ = error_details(exc)
assert category == "rate_limit"
def test_error_classification_does_not_mislabel_exhausted_quota_as_rate_limit():
exc = RuntimeError("429 insufficient_quota: check billing")
_, category, _ = error_details(exc)
assert category == "quota_or_balance"
class FakeAnthropicStream:
def __enter__(self):
return self
def __exit__(self, *_args):
return False
def __iter__(self):
return iter([
SimpleNamespace(
type="content_block_delta",
delta=SimpleNamespace(type="thinking_delta", thinking="reason"),
),
SimpleNamespace(
type="content_block_delta",
delta=SimpleNamespace(type="text_delta", text="answer"),
),
])
def get_final_message(self):
usage = SimpleNamespace(
input_tokens=60, output_tokens=10,
cache_creation_input_tokens=20, cache_read_input_tokens=20,
)
return SimpleNamespace(
id="anthropic-1", usage=usage, content=[], stop_reason="end_turn"
)
def test_native_anthropic_adapter_records_cache_and_ttft():
client = SimpleNamespace(
messages=SimpleNamespace(stream=lambda **_kwargs: FakeAnthropicStream())
)
row = measure_anthropic(
Provider("anthropic", "claude-test", "UNUSED", protocol="anthropic"),
campaign_id="c", phase="workload", cell_id="a", prompt="hello",
target_context_tokens=10, target_output_tokens=10, concurrency=1,
request_index=0, client=client,
)
assert row.ok
assert row.input_tokens == 100
assert row.cached_input_tokens == 20
assert row.output_tokens == 10
assert row.visible_output_tokens > 0
assert row.thinking_ttft_s is not None
def test_native_gemini_adapter_records_thought_tokens():
usage = SimpleNamespace(
prompt_token_count=50, cached_content_token_count=10,
candidates_token_count=8, thoughts_token_count=4,
)
parts = [
SimpleNamespace(text="reason", thought=True),
SimpleNamespace(text="answer", thought=False),
]
chunk = SimpleNamespace(
usage_metadata=usage,
candidates=[SimpleNamespace(
finish_reason="STOP",
content=SimpleNamespace(parts=parts),
)],
)
client = SimpleNamespace(
models=SimpleNamespace(generate_content_stream=lambda **_kwargs: iter([chunk]))
)
row = measure_gemini(
Provider("gemini", "gemini-test", "UNUSED", protocol="gemini"),
campaign_id="c", phase="workload", cell_id="g", prompt="hello",
target_context_tokens=10, target_output_tokens=10, concurrency=1,
request_index=0, client=client,
)
assert row.ok
assert row.input_tokens == 50
assert row.cached_input_tokens == 10
assert row.reasoning_tokens == 4
assert row.output_tokens == 12
assert row.visible_output_tokens == 8
def test_completion_requires_every_provider_availability_and_pinned_prices():
config = {
"workload": {"context_tokens": [8192], "output_tokens": [512]},
"availability": {"duration_hours": 168, "interval_seconds": 3600},
"rate_limit": {"concurrency_levels": [1], "requests_per_level": 1},
"agent_cost": {"rounds": 1},
"providers": [
{
"name": "provider-a", "model": "shared-model",
"pricing": {
"input_per_million": 1.0,
"cached_input_per_million": 0.1,
"output_per_million": 2.0,
"currency": "USD",
"source_url": "https://example.test/pricing",
"as_of": "2026-07-29",
"status": "verified",
},
},
{
"name": "provider-b", "model": "shared-model",
"pricing": {
"input_per_million": None,
"cached_input_per_million": None,
"output_per_million": None,
"currency": None,
"source_url": None,
"as_of": None,
"status": "unresolved",
"blocker": "exact public model price not found",
},
},
],
"same_model_provider_groups": [{
"logical_model": "shared-model",
"providers": ["provider-a", "provider-b"],
}],
}
workload = [{
"provider": "provider-a", "target_context_tokens": 8192,
"target_output_tokens": 512, "requests": 100, "successes": 100,
}]
availability = [{
"provider": "provider-a", "model": "shared-model", "probes": 169,
"observed_start_utc": "2026-01-01T00:00:00+00:00",
"observed_end_utc": "2026-01-08T00:00:00+00:00",
}]
audit = completion_audit(config, workload, availability, [], [], [])
assert audit["checks"]["configuration_matches_exact_8k_32k_128k_x_512_2048_design"] is False
assert audit["checks"]["availability_observed_for_at_least_168_hours"] is False
assert audit["missing_availability_providers"] == ["provider-b"]
assert audit["checks"]["cached_input_output_pricing_complete"] is False
assert audit["pricing_config_gaps"][0]["provider"] == "provider-b"
assert audit["checks"]["same_model_compared_across_providers"] is False
assert audit["same_model_provider_gaps"]
def test_non_usd_pricing_requires_dated_fx_for_comparable_cost():
price = Price(
20.0, 2.0, 100.0,
currency="CNY",
source_url="https://example.test/cny-pricing",
as_of="2026-07-29",
status="verified_native",
)
assert price.native_rates_complete is True
assert price.usd_conversion_complete is False
converted = Price(
20.0, 2.0, 100.0,
currency="CNY",
source_url="https://example.test/cny-pricing",
as_of="2026-07-29",
usd_per_currency_unit=0.139,
fx_source_url="https://example.test/fx",
fx_as_of="2026-07-29",
status="verified_with_fx",
)
assert converted.usd_conversion_complete is True
def test_markdown_tolerates_null_percentage_fields() -> None:
report = {
"campaign_id": "exp7-10-test",
"observation_count": 0,
"completion_audit": {"official_complete": False},
"workload": [
{
"provider": "openai",
"target_context_tokens": 8192,
"target_output_tokens": 512,
"requests": 0,
"success_rate": None,
"ttft_s": {"p50": None, "p95": None, "p99": None},
"e2e_s": {"p50": None, "p95": None, "p99": None},
"input_prefill_throughput_tokens_s": {"p50": None},
"output_throughput_tokens_s": {"p50": None},
"output_length_attainment_rate": None,
"reasoning_tokens": {"p50": None},
"thinking_ttft_s": {"p50": None},
}
],
"availability": [
{
"provider": "openai",
"probes": 0,
"uptime": None,
"outage_count": 0,
"mttr_s": None,
"longest_continuous_availability_s": 0.0,
}
],
"rate_limits": [
{
"provider": "openai",
"concurrency": 1,
"success_rate": None,
"measured_rpm": None,
"measured_input_tpm": None,
"measured_output_tpm": None,
}
],
"costs": [],
"external_benchmark_comparison": [],
}
result = markdown(report)
assert "| openai | 8192 | 512 | 0 | — | —/—/— | —/—/— | — | — | — | — | — |" in result
assert "| openai | 0 | — | 0 | — | 0.00 |" in result
assert "| openai | 1 | — | — | — | — |" in result
def test_export_campaign_summary_returns_report_and_markdown(tmp_path: Path) -> None:
from analysis import export_campaign_summary
store = CampaignStore(tmp_path / "summary.sqlite3")
store.bind_campaign("exp-test", {"providers": [], "workload": {}, "availability": {}, "rate_limit": {}, "agent_cost": {}})
store.close()
exported = export_campaign_summary(tmp_path / "summary.sqlite3", campaign_id="exp-test")
assert "report" in exported
assert "markdown" in exported
assert exported["official_complete"] is False