vlmeter 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- vlmeter-0.2.0/LICENSE +21 -0
- vlmeter-0.2.0/PKG-INFO +200 -0
- vlmeter-0.2.0/README.md +189 -0
- vlmeter-0.2.0/pyproject.toml +27 -0
- vlmeter-0.2.0/setup.cfg +4 -0
- vlmeter-0.2.0/tests/test_adapters.py +102 -0
- vlmeter-0.2.0/tests/test_scoring.py +103 -0
- vlmeter-0.2.0/vlmeter/__init__.py +3 -0
- vlmeter-0.2.0/vlmeter/__main__.py +3 -0
- vlmeter-0.2.0/vlmeter/cli.py +135 -0
- vlmeter-0.2.0/vlmeter/dataset.py +188 -0
- vlmeter-0.2.0/vlmeter/images.py +56 -0
- vlmeter-0.2.0/vlmeter/judge.py +75 -0
- vlmeter-0.2.0/vlmeter/models/__init__.py +61 -0
- vlmeter-0.2.0/vlmeter/models/anthropic_client.py +51 -0
- vlmeter-0.2.0/vlmeter/models/base.py +72 -0
- vlmeter-0.2.0/vlmeter/models/gemini_client.py +50 -0
- vlmeter-0.2.0/vlmeter/models/mock.py +40 -0
- vlmeter-0.2.0/vlmeter/models/ollama_client.py +42 -0
- vlmeter-0.2.0/vlmeter/models/openai_client.py +48 -0
- vlmeter-0.2.0/vlmeter/prompting.py +30 -0
- vlmeter-0.2.0/vlmeter/report.py +210 -0
- vlmeter-0.2.0/vlmeter/retry.py +31 -0
- vlmeter-0.2.0/vlmeter/runner.py +305 -0
- vlmeter-0.2.0/vlmeter/scoring.py +195 -0
- vlmeter-0.2.0/vlmeter.egg-info/PKG-INFO +200 -0
- vlmeter-0.2.0/vlmeter.egg-info/SOURCES.txt +29 -0
- vlmeter-0.2.0/vlmeter.egg-info/dependency_links.txt +1 -0
- vlmeter-0.2.0/vlmeter.egg-info/entry_points.txt +2 -0
- vlmeter-0.2.0/vlmeter.egg-info/requires.txt +6 -0
- vlmeter-0.2.0/vlmeter.egg-info/top_level.txt +1 -0
vlmeter-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 flybear16
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
vlmeter-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: vlmeter
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: 多模态大模型准确率 × 成本计量流水线:用自有数据评测视觉语言模型(场景检查/分类/VQA),输出准确率与成本对比报告
|
|
5
|
+
Author: flybear16
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: multimodal,vision-language,evaluation,benchmark,llm,cost
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
|
|
12
|
+
# vlmeter — 多模态大模型准确率 × 成本计量流水线
|
|
13
|
+
|
|
14
|
+
用自己的图片 + 标准答案,横向对比多个多模态模型(API 商用模型 + 本地开源模型)的准确率,自动出对比报告。
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
python3 -m vlmeter run --models deepseek-flash,gpt-4o,qwen2.5-vl-7b --data data/sample.jsonl --judge deepseek-v4-pro
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
**特性**
|
|
21
|
+
|
|
22
|
+
- 一套数据同时评多模型:OpenAI 兼容接口(GPT-4o / DeepSeek / GLM-4V / Qwen-VL / vLLM / sglang / LM Studio)、Anthropic Claude、Google Gemini、Ollama 本地模型,加模型只需在 `config/models.yaml` 加几行配置
|
|
23
|
+
- 三类任务:`classification`(分类/打标,选项匹配)、`checklist`(**一图多检查项**,如"有自行车吗+收银台干净吗",单次调用返回全部判定)、`vqa`(开放问答,exact / contains / numeric / LLM 裁判四种判分)
|
|
24
|
+
- 断点续跑:结果按题缓存,中断后重跑只补缺失项;失败请求自动重试
|
|
25
|
+
- **成本核算**:按模型单价 + 实际 token 用量估算,报告内置"成本与性能选型权衡表"(总成本/单次成本/每千次调用成本,准确率与成本并排对比)
|
|
26
|
+
- 报告:总准确率、分任务、**分检查项**、分类目、平均延迟、错误样本明细(Markdown),可直接对比
|
|
27
|
+
|
|
28
|
+
## 安装
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install -r requirements.txt # 本机已装可直接跳过
|
|
32
|
+
cp .env.example .env # 填入你有的 API 密钥(run 会自动加载)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## 快速上手(10 分钟跑通)
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
python3 scripts/make_sample_data.py # 生成 15 题合成示例数据(形状/颜色/计数/OCR)
|
|
39
|
+
python3 -m vlmeter list-models # 看当前可用的模型
|
|
40
|
+
python3 -m vlmeter run --models mock --data data/sample.jsonl # 离线自测,不花钱
|
|
41
|
+
python3 -m vlmeter run --models deepseek-flash --data data/sample.jsonl --judge deepseek-v4-pro
|
|
42
|
+
python3 -m vlmeter report --data data/sample.jsonl # 重新出报告
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
报告输出到 `results/report_*.md`,原始逐题结果在 `results/<模型名>/*.jsonl`。
|
|
46
|
+
|
|
47
|
+
## 接入你自己的数据
|
|
48
|
+
|
|
49
|
+
### 场景检查模式(推荐):一张图判断多种场景
|
|
50
|
+
|
|
51
|
+
典型需求:传一张图片,判断"有没有自行车"、"收银台干不干净"等多个是/否检查项。
|
|
52
|
+
一行数据 = 一张图 + 该图的全部检查项,**一次模型调用**返回所有判定(省 N-1 次调用):
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{"id": "shop001", "image": "images/shop001.jpg", "task": "checklist",
|
|
56
|
+
"checks": [
|
|
57
|
+
{"name": "图中有自行车", "answer": true},
|
|
58
|
+
{"name": "收银台干净整洁", "answer": false,
|
|
59
|
+
"criteria": "台面无散落垃圾、无明显污渍、无杂物堆放,轻微灰尘不算"},
|
|
60
|
+
{"name": "店内无杂物堆放", "answer": true}
|
|
61
|
+
]}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
- `answer` 是人工标注的真实情况(true/false)
|
|
65
|
+
- `name` 用短名(作报告列名和结果 ID);**主观/程度类检查务必加 `criteria` 判定标准**,只进提示词不进报告,用来对齐模型与标注员的标准——这是主观项准确率的最大影响因素
|
|
66
|
+
- 模型对每个检查项返回 `verdict`(是/否)+ `reason`(一句话图片依据),依据存入结果并在报告错题样本中展示,便于区分"模型真错了"还是"标准有歧义"
|
|
67
|
+
- 报告自动按**检查项名**分列统计准确率(如"收银台干净整洁 71%"),一眼看出哪个场景是弱项
|
|
68
|
+
- 解析兼容多种变体(纯"是/否"、布尔、数组);漏答/胡答按答错计并标注 `parse_fail`
|
|
69
|
+
|
|
70
|
+
### 单项提问模式
|
|
71
|
+
|
|
72
|
+
每行一个独立问题,适合不同图问不同事:
|
|
73
|
+
|
|
74
|
+
准备一个 JSONL(或同列 CSV),每行一题,图片路径相对数据集文件:
|
|
75
|
+
|
|
76
|
+
```json
|
|
77
|
+
{"id": "q001", "image": "images/001.jpg", "task": "classification",
|
|
78
|
+
"question": "这张图里的产品是哪种型号?", "choices": ["A型", "B型", "C型"],
|
|
79
|
+
"answer": "B型", "category": "型号识别"}
|
|
80
|
+
{"id": "q002", "image": "images/002.jpg", "task": "vqa",
|
|
81
|
+
"question": "图中破损位置在左上、右上、左下还是右下?", "answer": "左下",
|
|
82
|
+
"match": "judge", "category": "缺陷定位"}
|
|
83
|
+
{"id": "q003", "image": "images/003.jpg", "task": "vqa",
|
|
84
|
+
"question": "图中共有几个安全隐患点?", "answer": "3", "match": "numeric", "category": "计数"}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
字段说明:
|
|
88
|
+
|
|
89
|
+
| 字段 | 必填 | 说明 |
|
|
90
|
+
|---|---|---|
|
|
91
|
+
| `image` | ✅ | 图片路径;超过 4MB 或最长边超 1568px 会自动压缩 |
|
|
92
|
+
| `task` | | `classification`(需 `choices`)/ `checklist`(需 `checks`)/ `vqa`,默认 `vqa` |
|
|
93
|
+
| `question` | 分类/VQA 必填 | 提问文本 |
|
|
94
|
+
| `answer` | 分类/VQA 必填 | 标准答案。分类题填选项文本或对应字母 |
|
|
95
|
+
| `choices` | | 分类题选项列表 |
|
|
96
|
+
| `checks` | | checklist 检查项列表 `[{name, answer, criteria?}]`,answer 为 true/false,criteria 为可选判定标准 |
|
|
97
|
+
| `match` | | VQA 判分策略,默认有 judge 用 judge、否则 contains |
|
|
98
|
+
| `category` | | 业务分类,报告中分目统计(checklist 用检查项 name,无需填) |
|
|
99
|
+
| `tolerance` | | numeric 允许误差,默认 0 |
|
|
100
|
+
|
|
101
|
+
**VQA 判分策略怎么选**
|
|
102
|
+
|
|
103
|
+
- `numeric`:答案是数字(计数、读数)用这个,支持中文数字("三个")
|
|
104
|
+
- `exact`:答案唯一且无歧义(编号、是/否),忽略大小写和标点
|
|
105
|
+
- `contains`:标准答案需出现在回答里(宽松,适合关键词式答案)
|
|
106
|
+
- `judge`:LLM 裁判语义判分(推荐用于开放回答)。裁判只收到问题/标准答案/模型回答的**文本**,不会看图,所以任何纯文本模型都能当裁判。模型输出很长或语义开放时选这个最稳。
|
|
107
|
+
|
|
108
|
+
## 添加模型
|
|
109
|
+
|
|
110
|
+
编辑 `config/models.yaml`,加一段即可(把 `enabled` 改为 `true`):
|
|
111
|
+
|
|
112
|
+
```yaml
|
|
113
|
+
glm-4v:
|
|
114
|
+
enabled: true
|
|
115
|
+
provider: openai # OpenAI 兼容接口都用这个 provider
|
|
116
|
+
model: glm-4v-plus
|
|
117
|
+
base_url: https://open.bigmodel.cn/api/paas/v4/
|
|
118
|
+
api_key_env: ZHIPUAI_API_KEY # 从 .env / 环境变量读
|
|
119
|
+
concurrency: 4 # 并发请求数
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
本地模型两种方式:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
# 方式一:vLLM(推荐,OpenAI 兼容)
|
|
126
|
+
vllm serve Qwen/Qwen2.5-VL-7B-Instruct --port 8000
|
|
127
|
+
# 方式二:Ollama
|
|
128
|
+
ollama pull llava
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
对应 yaml 里已预置 `qwen2.5-vl-7b`(vLLM)和 `llava`(Ollama)条目,改 `enabled: true` 即可。
|
|
132
|
+
|
|
133
|
+
> 注意:若本机设置了 `OPENAI_BASE_URL` 代理(如本环境指向 DeepSeek),官方 OpenAI 条目必须写死 `base_url: https://api.openai.com/v1/` 并用独立密钥变量,避免把密钥发错服务端。
|
|
134
|
+
|
|
135
|
+
## 运行与报告
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
python3 -m vlmeter run --models a,b,c --data data/my.jsonl \
|
|
139
|
+
--judge deepseek-v4-pro \ # 裁判模型(可选,纯文本模型即可)
|
|
140
|
+
--limit 20 \ # 先跑前 20 题试水
|
|
141
|
+
--fresh # 忽略缓存全部重跑
|
|
142
|
+
python3 -m vlmeter report --data data/my.jsonl # 随时重新出报告
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
- 中断/限流失败不用怕:再执行同一条命令,只补失败和缺失的题
|
|
146
|
+
- 结果文件按数据集内容指纹隔离,换数据自动开新文件,不会串
|
|
147
|
+
- 推理型模型(如 DeepSeek)思考耗尽 `max_tokens` 会返回空答案,运行器会自动重试换一次采样;仍频繁出现可在该模型配置里调大 `max_tokens`
|
|
148
|
+
|
|
149
|
+
## 准确率与成本怎么看
|
|
150
|
+
|
|
151
|
+
两个核心指标都直接落在报告里,配合做选型决策:
|
|
152
|
+
|
|
153
|
+
**准确率**(多层下钻,避免被单一数字误导):
|
|
154
|
+
- 总览表:总体 / 分类 / 检查项 / VQA 分项准确率
|
|
155
|
+
- 分目表:**每个检查项一列**(如"收银台干净整洁 71%"),定位弱项场景
|
|
156
|
+
- 错误样本:标准答案 vs 模型输出 + 模型判定依据(reason),区分"真错"和"标准歧义"
|
|
157
|
+
|
|
158
|
+
**成本**(按实际用量核算,不是拍脑袋估):
|
|
159
|
+
- 在 `config/models.yaml` 给模型配 `price_input` / `price_output`(元/百万 tokens,⚠️ 示例价需按官网核对)
|
|
160
|
+
- 每次调用的输入/输出 tokens 从 API 响应实时记录;checklist 一图多检查项按**一次调用**计费(自动去重)
|
|
161
|
+
- 报告"成本与性能"表:本次总成本、单次调用成本、**每 1000 次调用成本**(生产量级外推)、平均延迟,与准确率并排
|
|
162
|
+
- 本地模型(vLLM/Ollama)配 `price: 0`,API 成本计 0(只有 GPU 电费,不在本工具口径内)
|
|
163
|
+
- judge 裁判调用费用另计(评测基础设施,不是被测模型成本)
|
|
164
|
+
|
|
165
|
+
**典型权衡读法**:模型 A 准确率 98% 每千次 ¥30,模型 B 准确率 93% 每千次 ¥3——若业务允许 93%(如人工复核兜底),B 省十倍;若错误代价高(如合规检查),A 更划算。报告给了数字,决策是业务的事。
|
|
166
|
+
|
|
167
|
+
**统计口径**
|
|
168
|
+
|
|
169
|
+
- **准确率** = 判对的判定点 / 已判定判定点(checklist 一图展开为多个判定点;请求失败和未判定的不计入分母,别把断线当"答错")
|
|
170
|
+
- **解析失败**:模型没按选项字母格式回答且无法映射回选项,按答错计(配置 judge 后裁判会把自由表述映射回选项,大幅减少这类误伤)
|
|
171
|
+
- **错误样本**:逐题列出标准答案 vs 模型输出,建议先看这个再下结论
|
|
172
|
+
|
|
173
|
+
## 项目结构
|
|
174
|
+
|
|
175
|
+
```
|
|
176
|
+
vlmeter/ 评测包
|
|
177
|
+
models/ 各家模型适配器(openai 兼容 / anthropic / gemini / ollama / mock)
|
|
178
|
+
dataset.py 数据集加载与校验
|
|
179
|
+
prompting.py 分类/VQA 提示模板
|
|
180
|
+
scoring.py 选项解析与 exact/contains/numeric 判分
|
|
181
|
+
judge.py LLM 裁判(VQA 语义判分 + 分类自由回答映射)
|
|
182
|
+
runner.py 异步并发、重试、断点续跑
|
|
183
|
+
report.py 聚合报告
|
|
184
|
+
config/models.yaml 模型注册表
|
|
185
|
+
scripts/make_sample_data.py 合成示例数据
|
|
186
|
+
tests/ 判分单元测试 + 适配器冒烟测试
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## 测试
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
python3 tests/test_scoring.py # 判分逻辑
|
|
193
|
+
python3 tests/test_adapters.py # 适配器请求格式(本地 mock 服务,不需要密钥)
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## 已知边界
|
|
197
|
+
|
|
198
|
+
- Ollama 适配器请求格式经过本地 mock 服务验证,但未在真实 Ollama 实例上跑过(本机无 GPU);vLLM 路径与 DeepSeek 同走 OpenAI 兼容适配器,已真实验证
|
|
199
|
+
- 裁判模型自身有判错概率;关键结论建议抽查报告中的错误样本
|
|
200
|
+
- 数据集指纹基于 JSONL/CSV 文件内容,单独替换同名图片不会触发重跑(用 `--fresh` 强制)
|
vlmeter-0.2.0/README.md
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# vlmeter — 多模态大模型准确率 × 成本计量流水线
|
|
2
|
+
|
|
3
|
+
用自己的图片 + 标准答案,横向对比多个多模态模型(API 商用模型 + 本地开源模型)的准确率,自动出对比报告。
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
python3 -m vlmeter run --models deepseek-flash,gpt-4o,qwen2.5-vl-7b --data data/sample.jsonl --judge deepseek-v4-pro
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
**特性**
|
|
10
|
+
|
|
11
|
+
- 一套数据同时评多模型:OpenAI 兼容接口(GPT-4o / DeepSeek / GLM-4V / Qwen-VL / vLLM / sglang / LM Studio)、Anthropic Claude、Google Gemini、Ollama 本地模型,加模型只需在 `config/models.yaml` 加几行配置
|
|
12
|
+
- 三类任务:`classification`(分类/打标,选项匹配)、`checklist`(**一图多检查项**,如"有自行车吗+收银台干净吗",单次调用返回全部判定)、`vqa`(开放问答,exact / contains / numeric / LLM 裁判四种判分)
|
|
13
|
+
- 断点续跑:结果按题缓存,中断后重跑只补缺失项;失败请求自动重试
|
|
14
|
+
- **成本核算**:按模型单价 + 实际 token 用量估算,报告内置"成本与性能选型权衡表"(总成本/单次成本/每千次调用成本,准确率与成本并排对比)
|
|
15
|
+
- 报告:总准确率、分任务、**分检查项**、分类目、平均延迟、错误样本明细(Markdown),可直接对比
|
|
16
|
+
|
|
17
|
+
## 安装
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install -r requirements.txt # 本机已装可直接跳过
|
|
21
|
+
cp .env.example .env # 填入你有的 API 密钥(run 会自动加载)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## 快速上手(10 分钟跑通)
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
python3 scripts/make_sample_data.py # 生成 15 题合成示例数据(形状/颜色/计数/OCR)
|
|
28
|
+
python3 -m vlmeter list-models # 看当前可用的模型
|
|
29
|
+
python3 -m vlmeter run --models mock --data data/sample.jsonl # 离线自测,不花钱
|
|
30
|
+
python3 -m vlmeter run --models deepseek-flash --data data/sample.jsonl --judge deepseek-v4-pro
|
|
31
|
+
python3 -m vlmeter report --data data/sample.jsonl # 重新出报告
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
报告输出到 `results/report_*.md`,原始逐题结果在 `results/<模型名>/*.jsonl`。
|
|
35
|
+
|
|
36
|
+
## 接入你自己的数据
|
|
37
|
+
|
|
38
|
+
### 场景检查模式(推荐):一张图判断多种场景
|
|
39
|
+
|
|
40
|
+
典型需求:传一张图片,判断"有没有自行车"、"收银台干不干净"等多个是/否检查项。
|
|
41
|
+
一行数据 = 一张图 + 该图的全部检查项,**一次模型调用**返回所有判定(省 N-1 次调用):
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{"id": "shop001", "image": "images/shop001.jpg", "task": "checklist",
|
|
45
|
+
"checks": [
|
|
46
|
+
{"name": "图中有自行车", "answer": true},
|
|
47
|
+
{"name": "收银台干净整洁", "answer": false,
|
|
48
|
+
"criteria": "台面无散落垃圾、无明显污渍、无杂物堆放,轻微灰尘不算"},
|
|
49
|
+
{"name": "店内无杂物堆放", "answer": true}
|
|
50
|
+
]}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
- `answer` 是人工标注的真实情况(true/false)
|
|
54
|
+
- `name` 用短名(作报告列名和结果 ID);**主观/程度类检查务必加 `criteria` 判定标准**,只进提示词不进报告,用来对齐模型与标注员的标准——这是主观项准确率的最大影响因素
|
|
55
|
+
- 模型对每个检查项返回 `verdict`(是/否)+ `reason`(一句话图片依据),依据存入结果并在报告错题样本中展示,便于区分"模型真错了"还是"标准有歧义"
|
|
56
|
+
- 报告自动按**检查项名**分列统计准确率(如"收银台干净整洁 71%"),一眼看出哪个场景是弱项
|
|
57
|
+
- 解析兼容多种变体(纯"是/否"、布尔、数组);漏答/胡答按答错计并标注 `parse_fail`
|
|
58
|
+
|
|
59
|
+
### 单项提问模式
|
|
60
|
+
|
|
61
|
+
每行一个独立问题,适合不同图问不同事:
|
|
62
|
+
|
|
63
|
+
准备一个 JSONL(或同列 CSV),每行一题,图片路径相对数据集文件:
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{"id": "q001", "image": "images/001.jpg", "task": "classification",
|
|
67
|
+
"question": "这张图里的产品是哪种型号?", "choices": ["A型", "B型", "C型"],
|
|
68
|
+
"answer": "B型", "category": "型号识别"}
|
|
69
|
+
{"id": "q002", "image": "images/002.jpg", "task": "vqa",
|
|
70
|
+
"question": "图中破损位置在左上、右上、左下还是右下?", "answer": "左下",
|
|
71
|
+
"match": "judge", "category": "缺陷定位"}
|
|
72
|
+
{"id": "q003", "image": "images/003.jpg", "task": "vqa",
|
|
73
|
+
"question": "图中共有几个安全隐患点?", "answer": "3", "match": "numeric", "category": "计数"}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
字段说明:
|
|
77
|
+
|
|
78
|
+
| 字段 | 必填 | 说明 |
|
|
79
|
+
|---|---|---|
|
|
80
|
+
| `image` | ✅ | 图片路径;超过 4MB 或最长边超 1568px 会自动压缩 |
|
|
81
|
+
| `task` | | `classification`(需 `choices`)/ `checklist`(需 `checks`)/ `vqa`,默认 `vqa` |
|
|
82
|
+
| `question` | 分类/VQA 必填 | 提问文本 |
|
|
83
|
+
| `answer` | 分类/VQA 必填 | 标准答案。分类题填选项文本或对应字母 |
|
|
84
|
+
| `choices` | | 分类题选项列表 |
|
|
85
|
+
| `checks` | | checklist 检查项列表 `[{name, answer, criteria?}]`,answer 为 true/false,criteria 为可选判定标准 |
|
|
86
|
+
| `match` | | VQA 判分策略,默认有 judge 用 judge、否则 contains |
|
|
87
|
+
| `category` | | 业务分类,报告中分目统计(checklist 用检查项 name,无需填) |
|
|
88
|
+
| `tolerance` | | numeric 允许误差,默认 0 |
|
|
89
|
+
|
|
90
|
+
**VQA 判分策略怎么选**
|
|
91
|
+
|
|
92
|
+
- `numeric`:答案是数字(计数、读数)用这个,支持中文数字("三个")
|
|
93
|
+
- `exact`:答案唯一且无歧义(编号、是/否),忽略大小写和标点
|
|
94
|
+
- `contains`:标准答案需出现在回答里(宽松,适合关键词式答案)
|
|
95
|
+
- `judge`:LLM 裁判语义判分(推荐用于开放回答)。裁判只收到问题/标准答案/模型回答的**文本**,不会看图,所以任何纯文本模型都能当裁判。模型输出很长或语义开放时选这个最稳。
|
|
96
|
+
|
|
97
|
+
## 添加模型
|
|
98
|
+
|
|
99
|
+
编辑 `config/models.yaml`,加一段即可(把 `enabled` 改为 `true`):
|
|
100
|
+
|
|
101
|
+
```yaml
|
|
102
|
+
glm-4v:
|
|
103
|
+
enabled: true
|
|
104
|
+
provider: openai # OpenAI 兼容接口都用这个 provider
|
|
105
|
+
model: glm-4v-plus
|
|
106
|
+
base_url: https://open.bigmodel.cn/api/paas/v4/
|
|
107
|
+
api_key_env: ZHIPUAI_API_KEY # 从 .env / 环境变量读
|
|
108
|
+
concurrency: 4 # 并发请求数
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
本地模型两种方式:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
# 方式一:vLLM(推荐,OpenAI 兼容)
|
|
115
|
+
vllm serve Qwen/Qwen2.5-VL-7B-Instruct --port 8000
|
|
116
|
+
# 方式二:Ollama
|
|
117
|
+
ollama pull llava
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
对应 yaml 里已预置 `qwen2.5-vl-7b`(vLLM)和 `llava`(Ollama)条目,改 `enabled: true` 即可。
|
|
121
|
+
|
|
122
|
+
> 注意:若本机设置了 `OPENAI_BASE_URL` 代理(如本环境指向 DeepSeek),官方 OpenAI 条目必须写死 `base_url: https://api.openai.com/v1/` 并用独立密钥变量,避免把密钥发错服务端。
|
|
123
|
+
|
|
124
|
+
## 运行与报告
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
python3 -m vlmeter run --models a,b,c --data data/my.jsonl \
|
|
128
|
+
--judge deepseek-v4-pro \ # 裁判模型(可选,纯文本模型即可)
|
|
129
|
+
--limit 20 \ # 先跑前 20 题试水
|
|
130
|
+
--fresh # 忽略缓存全部重跑
|
|
131
|
+
python3 -m vlmeter report --data data/my.jsonl # 随时重新出报告
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
- 中断/限流失败不用怕:再执行同一条命令,只补失败和缺失的题
|
|
135
|
+
- 结果文件按数据集内容指纹隔离,换数据自动开新文件,不会串
|
|
136
|
+
- 推理型模型(如 DeepSeek)思考耗尽 `max_tokens` 会返回空答案,运行器会自动重试换一次采样;仍频繁出现可在该模型配置里调大 `max_tokens`
|
|
137
|
+
|
|
138
|
+
## 准确率与成本怎么看
|
|
139
|
+
|
|
140
|
+
两个核心指标都直接落在报告里,配合做选型决策:
|
|
141
|
+
|
|
142
|
+
**准确率**(多层下钻,避免被单一数字误导):
|
|
143
|
+
- 总览表:总体 / 分类 / 检查项 / VQA 分项准确率
|
|
144
|
+
- 分目表:**每个检查项一列**(如"收银台干净整洁 71%"),定位弱项场景
|
|
145
|
+
- 错误样本:标准答案 vs 模型输出 + 模型判定依据(reason),区分"真错"和"标准歧义"
|
|
146
|
+
|
|
147
|
+
**成本**(按实际用量核算,不是拍脑袋估):
|
|
148
|
+
- 在 `config/models.yaml` 给模型配 `price_input` / `price_output`(元/百万 tokens,⚠️ 示例价需按官网核对)
|
|
149
|
+
- 每次调用的输入/输出 tokens 从 API 响应实时记录;checklist 一图多检查项按**一次调用**计费(自动去重)
|
|
150
|
+
- 报告"成本与性能"表:本次总成本、单次调用成本、**每 1000 次调用成本**(生产量级外推)、平均延迟,与准确率并排
|
|
151
|
+
- 本地模型(vLLM/Ollama)配 `price: 0`,API 成本计 0(只有 GPU 电费,不在本工具口径内)
|
|
152
|
+
- judge 裁判调用费用另计(评测基础设施,不是被测模型成本)
|
|
153
|
+
|
|
154
|
+
**典型权衡读法**:模型 A 准确率 98% 每千次 ¥30,模型 B 准确率 93% 每千次 ¥3——若业务允许 93%(如人工复核兜底),B 省十倍;若错误代价高(如合规检查),A 更划算。报告给了数字,决策是业务的事。
|
|
155
|
+
|
|
156
|
+
**统计口径**
|
|
157
|
+
|
|
158
|
+
- **准确率** = 判对的判定点 / 已判定判定点(checklist 一图展开为多个判定点;请求失败和未判定的不计入分母,别把断线当"答错")
|
|
159
|
+
- **解析失败**:模型没按选项字母格式回答且无法映射回选项,按答错计(配置 judge 后裁判会把自由表述映射回选项,大幅减少这类误伤)
|
|
160
|
+
- **错误样本**:逐题列出标准答案 vs 模型输出,建议先看这个再下结论
|
|
161
|
+
|
|
162
|
+
## 项目结构
|
|
163
|
+
|
|
164
|
+
```
|
|
165
|
+
vlmeter/ 评测包
|
|
166
|
+
models/ 各家模型适配器(openai 兼容 / anthropic / gemini / ollama / mock)
|
|
167
|
+
dataset.py 数据集加载与校验
|
|
168
|
+
prompting.py 分类/VQA 提示模板
|
|
169
|
+
scoring.py 选项解析与 exact/contains/numeric 判分
|
|
170
|
+
judge.py LLM 裁判(VQA 语义判分 + 分类自由回答映射)
|
|
171
|
+
runner.py 异步并发、重试、断点续跑
|
|
172
|
+
report.py 聚合报告
|
|
173
|
+
config/models.yaml 模型注册表
|
|
174
|
+
scripts/make_sample_data.py 合成示例数据
|
|
175
|
+
tests/ 判分单元测试 + 适配器冒烟测试
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## 测试
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
python3 tests/test_scoring.py # 判分逻辑
|
|
182
|
+
python3 tests/test_adapters.py # 适配器请求格式(本地 mock 服务,不需要密钥)
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## 已知边界
|
|
186
|
+
|
|
187
|
+
- Ollama 适配器请求格式经过本地 mock 服务验证,但未在真实 Ollama 实例上跑过(本机无 GPU);vLLM 路径与 DeepSeek 同走 OpenAI 兼容适配器,已真实验证
|
|
188
|
+
- 裁判模型自身有判错概率;关键结论建议抽查报告中的错误样本
|
|
189
|
+
- 数据集指纹基于 JSONL/CSV 文件内容,单独替换同名图片不会触发重跑(用 `--fresh` 强制)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "vlmeter"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "多模态大模型准确率 × 成本计量流水线:用自有数据评测视觉语言模型(场景检查/分类/VQA),输出准确率与成本对比报告"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "flybear16" }]
|
|
13
|
+
keywords = ["multimodal", "vision-language", "evaluation", "benchmark", "llm", "cost"]
|
|
14
|
+
dependencies = [
|
|
15
|
+
"openai>=1.40",
|
|
16
|
+
"httpx>=0.27",
|
|
17
|
+
"pyyaml>=6.0",
|
|
18
|
+
"pandas>=2.0",
|
|
19
|
+
"pillow>=10.0",
|
|
20
|
+
"tqdm>=4.66",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.scripts]
|
|
24
|
+
vlmeter = "vlmeter.cli:main"
|
|
25
|
+
|
|
26
|
+
[tool.setuptools.packages.find]
|
|
27
|
+
include = ["vlmeter*"]
|
vlmeter-0.2.0/setup.cfg
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Adapter smoke tests against canned local HTTP endpoints.
|
|
2
|
+
|
|
3
|
+
Validates request shape and response parsing for the anthropic / gemini /
|
|
4
|
+
ollama adapters without needing real keys or a GPU. Run:
|
|
5
|
+
python tests/test_adapters.py
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import base64
|
|
11
|
+
import json
|
|
12
|
+
import sys
|
|
13
|
+
import threading
|
|
14
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
18
|
+
|
|
19
|
+
from vlmeter.models.anthropic_client import AnthropicModel
|
|
20
|
+
from vlmeter.models.base import ModelConfig
|
|
21
|
+
from vlmeter.models.gemini_client import GeminiModel
|
|
22
|
+
from vlmeter.models.ollama_client import OllamaModel
|
|
23
|
+
|
|
24
|
+
RECEIVED: dict[str, dict] = {}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Handler(BaseHTTPRequestHandler):
|
|
28
|
+
def do_POST(self): # noqa: N802
|
|
29
|
+
n = int(self.headers.get("content-length", 0))
|
|
30
|
+
body = json.loads(self.rfile.read(n))
|
|
31
|
+
route = (
|
|
32
|
+
"anthropic" if self.path.startswith("/v1/messages")
|
|
33
|
+
else "gemini" if ":generateContent" in self.path
|
|
34
|
+
else "ollama"
|
|
35
|
+
)
|
|
36
|
+
RECEIVED[route] = {"path": self.path, "body": body, "headers": dict(self.headers)}
|
|
37
|
+
if route == "anthropic":
|
|
38
|
+
payload = {"content": [{"type": "text", "text": "A"}],
|
|
39
|
+
"usage": {"input_tokens": 10, "output_tokens": 1}}
|
|
40
|
+
elif route == "gemini":
|
|
41
|
+
payload = {"candidates": [{"content": {"parts": [{"text": "B"}]}}],
|
|
42
|
+
"usageMetadata": {"totalTokenCount": 11}}
|
|
43
|
+
else:
|
|
44
|
+
payload = {"message": {"content": "C"}, "eval_count": 9}
|
|
45
|
+
data = json.dumps(payload).encode()
|
|
46
|
+
self.send_response(200)
|
|
47
|
+
self.send_header("content-type", "application/json")
|
|
48
|
+
self.send_header("content-length", str(len(data)))
|
|
49
|
+
self.end_headers()
|
|
50
|
+
self.wfile.write(data)
|
|
51
|
+
|
|
52
|
+
def log_message(self, *a): # silence
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
IMG = Path(__file__).resolve().parents[1] / "data" / "images" / "cls_shape_00.png"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def main() -> None:
|
|
60
|
+
if not IMG.exists():
|
|
61
|
+
sys.exit("run scripts/make_sample_data.py first")
|
|
62
|
+
srv = HTTPServer(("127.0.0.1", 0), Handler)
|
|
63
|
+
port = srv.server_address[1]
|
|
64
|
+
threading.Thread(target=srv.serve_forever, daemon=True).start()
|
|
65
|
+
|
|
66
|
+
cases = [
|
|
67
|
+
(AnthropicModel, "anthropic"),
|
|
68
|
+
(GeminiModel, "gemini"),
|
|
69
|
+
(OllamaModel, "ollama"),
|
|
70
|
+
]
|
|
71
|
+
for cls, route in cases:
|
|
72
|
+
cfg = ModelConfig(name=f"t-{route}", provider=route, model="m",
|
|
73
|
+
base_url=f"http://127.0.0.1:{port}")
|
|
74
|
+
resp = asyncio.run(cls(cfg).chat(str(IMG), "test"))
|
|
75
|
+
assert resp.text in ("A", "B", "C"), f"{route}: bad text {resp.text!r}"
|
|
76
|
+
body = RECEIVED[route]["body"]
|
|
77
|
+
if route == "anthropic":
|
|
78
|
+
assert body["messages"][0]["content"][0]["type"] == "image"
|
|
79
|
+
assert body["messages"][0]["content"][0]["source"]["media_type"] == "image/png"
|
|
80
|
+
base64.b64decode(body["messages"][0]["content"][0]["source"]["data"])
|
|
81
|
+
elif route == "gemini":
|
|
82
|
+
part = body["contents"][0]["parts"][0]["inline_data"]
|
|
83
|
+
base64.b64decode(part["data"])
|
|
84
|
+
assert part["mime_type"] == "image/png"
|
|
85
|
+
else:
|
|
86
|
+
base64.b64decode(body["messages"][0]["images"][0])
|
|
87
|
+
assert body["stream"] is False
|
|
88
|
+
print(f"{route} adapter OK (latency {resp.latency_ms}ms, tokens {resp.tokens})")
|
|
89
|
+
|
|
90
|
+
# text-only call (judge path) must not include any image block
|
|
91
|
+
cfg = ModelConfig(name="t-an2", provider="anthropic", model="m",
|
|
92
|
+
base_url=f"http://127.0.0.1:{port}")
|
|
93
|
+
asyncio.run(AnthropicModel(cfg).chat(None, "judge"))
|
|
94
|
+
assert all(b["type"] == "text"
|
|
95
|
+
for b in RECEIVED["anthropic"]["body"]["messages"][0]["content"])
|
|
96
|
+
print("text-only (judge) call OK")
|
|
97
|
+
srv.shutdown()
|
|
98
|
+
print("all adapter tests passed")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
if __name__ == "__main__":
|
|
102
|
+
main()
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Scoring unit tests. Run: python tests/test_scoring.py (or pytest)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
8
|
+
|
|
9
|
+
from vlmeter.dataset import Item
|
|
10
|
+
from vlmeter.scoring import extract_numbers, normalize, parse_checklist, parse_choice, score
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _cls(answer: str, choices: list[str]) -> Item:
|
|
14
|
+
return Item(id="t", image="x", task="classification", question="q",
|
|
15
|
+
choices=choices, answer=answer)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _vqa(answer: str, match: str) -> Item:
|
|
19
|
+
return Item(id="t", image="x", task="vqa", question="q", answer=answer, match=match)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_parse_choice():
|
|
23
|
+
ch = ["猫", "狗", "鸟"]
|
|
24
|
+
assert parse_choice("A", ch) == 0
|
|
25
|
+
assert parse_choice("B.", ch) == 1
|
|
26
|
+
assert parse_choice("答案是 C", ch) == 2
|
|
27
|
+
assert parse_choice("(A)", ch) == 0
|
|
28
|
+
assert parse_choice("这是一只猫", ch) == 0
|
|
29
|
+
assert parse_choice("我觉得是狗。", ch) == 1
|
|
30
|
+
assert parse_choice("不知道", ch) is None
|
|
31
|
+
# 模型枚举选项后再给结论:结论必须赢,不能取第一个字母
|
|
32
|
+
assert parse_choice("A 为圆形,B 为方形。答案是B。", ch) == 1
|
|
33
|
+
assert parse_choice("A. 猫 不符;B. 狗 符合。因此,答案是**B**", ch) == 1
|
|
34
|
+
assert parse_choice("A. 猫: no; C. 鸟: yes, 故选C", ch) == 2
|
|
35
|
+
assert parse_choice("answer is B", ch) == 1
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_classification_score():
|
|
39
|
+
item = _cls("圆形", ["圆形", "方形", "三角形"])
|
|
40
|
+
assert score(item, "A").correct is True
|
|
41
|
+
assert score(item, "三角形").correct is False
|
|
42
|
+
assert score(item, "看不清").correct is False
|
|
43
|
+
assert score(item, "看不清").reason == "parse_fail"
|
|
44
|
+
# answer given as letter
|
|
45
|
+
item2 = _cls("B", ["圆形", "方形", "三角形"])
|
|
46
|
+
assert score(item2, "方形").correct is True
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_vqa_match_modes():
|
|
50
|
+
assert score(_vqa("3", "exact"), "3 个").correct is False
|
|
51
|
+
assert score(_vqa("ZK-2049", "exact"), "zk2049").correct is True # 忽略大小写/标点
|
|
52
|
+
assert score(_vqa("ZK-2049", "exact"), "ZK-2048").correct is False
|
|
53
|
+
assert score(_vqa("红色", "contains"), "图中车辆是红色的。").correct is True
|
|
54
|
+
n = score(_vqa("3", "numeric"), "图中共有三个圆形")
|
|
55
|
+
assert n.correct is True and n.match == "numeric"
|
|
56
|
+
assert score(_vqa("12.5", "numeric"), "约 12.50").correct is True
|
|
57
|
+
assert score(_vqa("3", "numeric"), "4").correct is False
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_extract_numbers_chinese():
|
|
61
|
+
assert extract_numbers("二十三个") == [23.0]
|
|
62
|
+
assert extract_numbers("一共有5个") == [5.0]
|
|
63
|
+
assert extract_numbers("十") == [10.0]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_normalize():
|
|
67
|
+
assert normalize(" Hello, World! ") == "helloworld"
|
|
68
|
+
assert normalize("3只") == normalize("3只")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_parse_checklist():
|
|
72
|
+
# 提示词要求的格式:verdict + reason
|
|
73
|
+
v = parse_checklist('{"1": {"verdict": "是", "reason": "左下角有自行车"}, '
|
|
74
|
+
'"2": {"verdict": "否", "reason": "台面有污渍"}}', 2)
|
|
75
|
+
assert v == [(True, "左下角有自行车"), (False, "台面有污渍")]
|
|
76
|
+
# 变体:数组、布尔、纯字符串(无理由)
|
|
77
|
+
assert parse_checklist('{"1": ["是", "依据"], "2": ["否"]}', 2) == [(True, "依据"), (False, "")]
|
|
78
|
+
assert parse_checklist('{"1": true, "2": false}', 2) == [(True, ""), (False, "")]
|
|
79
|
+
assert parse_checklist('{"1": "是", "2": "否", "3": "是"}', 3) == [(True, ""), (False, ""), (True, "")]
|
|
80
|
+
# JSON 前后有废话 / markdown 代码块
|
|
81
|
+
assert parse_checklist('结论:```json\n{"1": {"verdict": "是", "reason": "可见"}}\n```', 1) == [(True, "可见")]
|
|
82
|
+
# 漏答的检查项为 (None, "")
|
|
83
|
+
assert parse_checklist('{"1": "是"}', 2) == [(True, ""), (None, "")]
|
|
84
|
+
# 完全不是 JSON / 非法值
|
|
85
|
+
assert parse_checklist("这张图很干净。", 2) == [(None, ""), (None, "")]
|
|
86
|
+
assert parse_checklist('{"1": {"verdict": "也许", "reason": "看不清"}}', 1) == [(None, "看不清")]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_cost_estimate():
|
|
90
|
+
from vlmeter.models.base import ModelConfig
|
|
91
|
+
cfg = ModelConfig(name="t", provider="mock", model="m",
|
|
92
|
+
price_input=0.5, price_output=2.0)
|
|
93
|
+
assert abs(cfg.estimate_cost(1_000_000, 1_000_000) - 2.5) < 1e-9
|
|
94
|
+
assert abs(cfg.estimate_cost(1000, 500) - (0.5 * 1000 + 2.0 * 500) / 1e6) < 1e-12
|
|
95
|
+
assert ModelConfig(name="t2", provider="mock", model="m").estimate_cost(1, 1) is None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
if __name__ == "__main__":
|
|
99
|
+
for name, fn in sorted(globals().items()):
|
|
100
|
+
if name.startswith("test_"):
|
|
101
|
+
fn()
|
|
102
|
+
print(f"{name} PASSED")
|
|
103
|
+
print("all scoring tests passed")
|