lab-cost-tracker 0.1.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.
- lab_cost_tracker-0.1.0/LICENSE +21 -0
- lab_cost_tracker-0.1.0/PKG-INFO +254 -0
- lab_cost_tracker-0.1.0/README.md +219 -0
- lab_cost_tracker-0.1.0/lab_cost_tracker/__init__.py +7 -0
- lab_cost_tracker-0.1.0/lab_cost_tracker/auth.py +162 -0
- lab_cost_tracker-0.1.0/lab_cost_tracker/config.py +82 -0
- lab_cost_tracker-0.1.0/lab_cost_tracker/pricing.py +99 -0
- lab_cost_tracker-0.1.0/lab_cost_tracker/storage.py +144 -0
- lab_cost_tracker-0.1.0/lab_cost_tracker/sync.py +111 -0
- lab_cost_tracker-0.1.0/lab_cost_tracker/tracker.py +191 -0
- lab_cost_tracker-0.1.0/lab_cost_tracker/wrapper.py +126 -0
- lab_cost_tracker-0.1.0/lab_cost_tracker.egg-info/PKG-INFO +254 -0
- lab_cost_tracker-0.1.0/lab_cost_tracker.egg-info/SOURCES.txt +17 -0
- lab_cost_tracker-0.1.0/lab_cost_tracker.egg-info/dependency_links.txt +1 -0
- lab_cost_tracker-0.1.0/lab_cost_tracker.egg-info/requires.txt +24 -0
- lab_cost_tracker-0.1.0/lab_cost_tracker.egg-info/top_level.txt +2 -0
- lab_cost_tracker-0.1.0/pyproject.toml +39 -0
- lab_cost_tracker-0.1.0/setup.cfg +4 -0
- lab_cost_tracker-0.1.0/tests/test_all_providers.py +239 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 University of Auckland
|
|
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.
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lab-cost-tracker
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Lightweight API cost tracker for research labs
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Repository, https://github.com/your-org/lab-cost-tracker
|
|
7
|
+
Keywords: llm,cost-tracking,openai,gemini,anthropic
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Science/Research
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: pyyaml>=6.0
|
|
17
|
+
Requires-Dist: requests>=2.31
|
|
18
|
+
Provides-Extra: openai
|
|
19
|
+
Requires-Dist: openai>=1.0; extra == "openai"
|
|
20
|
+
Provides-Extra: gemini
|
|
21
|
+
Requires-Dist: google-genai>=1.0; extra == "gemini"
|
|
22
|
+
Provides-Extra: anthropic
|
|
23
|
+
Requires-Dist: anthropic>=0.30; extra == "anthropic"
|
|
24
|
+
Provides-Extra: google
|
|
25
|
+
Requires-Dist: google-auth>=2.0; extra == "google"
|
|
26
|
+
Requires-Dist: google-auth-oauthlib>=1.0; extra == "google"
|
|
27
|
+
Provides-Extra: all
|
|
28
|
+
Requires-Dist: openai>=1.0; extra == "all"
|
|
29
|
+
Requires-Dist: google-genai>=1.0; extra == "all"
|
|
30
|
+
Requires-Dist: anthropic>=0.30; extra == "all"
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
33
|
+
Requires-Dist: pytest-mock; extra == "dev"
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
|
|
36
|
+
# Lab Cost Tracker SDK
|
|
37
|
+
|
|
38
|
+
轻量级 Python SDK,为研究组透明追踪 LLM API 调用费用。一行 `tracker.wrap()` 包裹现有 client,自动记录每次调用的模型、token 数和费用。
|
|
39
|
+
|
|
40
|
+
## 支持的 Provider
|
|
41
|
+
|
|
42
|
+
| Provider | Client 类型 | 拦截方法 |
|
|
43
|
+
|----------|------------|---------|
|
|
44
|
+
| OpenAI 官方 | `openai.OpenAI` | `chat.completions.create()` |
|
|
45
|
+
| Google Gemini | `google.genai.Client` | `models.generate_content()` |
|
|
46
|
+
| Anthropic | `anthropic.Anthropic` | `messages.create()` |
|
|
47
|
+
| 第三方代理 | `openai.OpenAI(base_url=...)` | 同 OpenAI(自动记录 `base_url`) |
|
|
48
|
+
|
|
49
|
+
## 安装
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
# 基础安装(仅需 pyyaml + requests)
|
|
53
|
+
pip install -e .
|
|
54
|
+
|
|
55
|
+
# 安装 OpenAI 支持
|
|
56
|
+
pip install -e ".[openai]"
|
|
57
|
+
|
|
58
|
+
# 安装 Google OAuth 支持(可选,用于身份验证)
|
|
59
|
+
pip install -e ".[google]"
|
|
60
|
+
|
|
61
|
+
# 全部安装
|
|
62
|
+
pip install -e ".[openai,google]"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## 快速开始
|
|
66
|
+
|
|
67
|
+
### 1. OpenAI
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from openai import OpenAI
|
|
71
|
+
from lab_cost_tracker import CostTracker
|
|
72
|
+
|
|
73
|
+
tracker = CostTracker(
|
|
74
|
+
project="DentalVLM",
|
|
75
|
+
user="kyle@aucklanduni.ac.nz",
|
|
76
|
+
storage_dir="./.cost-tracker", # 费用数据保存目录(默认值,可省略)
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
client = tracker.wrap(OpenAI(api_key="sk-..."))
|
|
80
|
+
|
|
81
|
+
# 正常使用,费用自动记录
|
|
82
|
+
response = client.chat.completions.create(
|
|
83
|
+
model="gpt-4o-mini",
|
|
84
|
+
messages=[{"role": "user", "content": "Hello!"}],
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
print(tracker.summary())
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### 2. Google Gemini
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from google import genai
|
|
94
|
+
from lab_cost_tracker import CostTracker
|
|
95
|
+
|
|
96
|
+
tracker = CostTracker(project="DentalVLM", user="kyle@aucklanduni.ac.nz")
|
|
97
|
+
|
|
98
|
+
client = tracker.wrap(genai.Client(api_key="..."))
|
|
99
|
+
|
|
100
|
+
response = client.models.generate_content(
|
|
101
|
+
model="gemini-2.5-flash",
|
|
102
|
+
contents="用一句话介绍新西兰奥克兰大学。",
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
print(tracker.summary())
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### 3. Anthropic
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
import anthropic
|
|
112
|
+
from lab_cost_tracker import CostTracker
|
|
113
|
+
|
|
114
|
+
tracker = CostTracker(project="DentalVLM", user="kyle@aucklanduni.ac.nz")
|
|
115
|
+
|
|
116
|
+
client = tracker.wrap(anthropic.Anthropic(api_key="..."))
|
|
117
|
+
|
|
118
|
+
response = client.messages.create(
|
|
119
|
+
model="claude-3-5-sonnet-20241022",
|
|
120
|
+
max_tokens=1024,
|
|
121
|
+
messages=[{"role": "user", "content": "Hello!"}],
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
print(tracker.summary())
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### 4. 第三方代理(如 apiyihe.org)
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from openai import OpenAI
|
|
131
|
+
from lab_cost_tracker import CostTracker
|
|
132
|
+
|
|
133
|
+
tracker = CostTracker(project="DentalVLM", user="kyle@aucklanduni.ac.nz")
|
|
134
|
+
|
|
135
|
+
# 使用 OpenAI SDK + 自定义 base_url
|
|
136
|
+
client = tracker.wrap(OpenAI(
|
|
137
|
+
api_key="...",
|
|
138
|
+
base_url="https://z.apiyihe.org/v1",
|
|
139
|
+
))
|
|
140
|
+
|
|
141
|
+
# 可以调用任何模型,base_url 会自动记录在 detail.jsonl 中
|
|
142
|
+
response = client.chat.completions.create(
|
|
143
|
+
model="gemini-2.5-flash-lite-preview-06-17",
|
|
144
|
+
messages=[{"role": "user", "content": "Hello!"}],
|
|
145
|
+
)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### 5. 手动记录(不支持的 Provider)
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
tracker.record(
|
|
152
|
+
model="llama-3-8b",
|
|
153
|
+
prompt_tokens=1000,
|
|
154
|
+
completion_tokens=500,
|
|
155
|
+
task="inference",
|
|
156
|
+
dataset="DentalBench",
|
|
157
|
+
)
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## 数据存储
|
|
161
|
+
|
|
162
|
+
运行后会在 `storage_dir`(默认 `{项目根目录}/.cost-tracker/`)下生成两个文件:
|
|
163
|
+
|
|
164
|
+
### `detail.jsonl` — 逐条调用记录
|
|
165
|
+
|
|
166
|
+
```json
|
|
167
|
+
{"id": "42ae...", "timestamp": "2026-05-19T...", "project": "DentalVLM", "user": "kyle@aucklanduni.ac.nz", "model": "gpt-4o-mini-2024-07-18", "prompt_tokens": 19, "completion_tokens": 33, "cost_usd": 0.00059, "metadata": {"provider": "openai", "base_url": "https://api.openai.com/v1"}, "synced": false}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### `summary.json` — 聚合统计
|
|
171
|
+
|
|
172
|
+
```json
|
|
173
|
+
{
|
|
174
|
+
"project": "DentalVLM",
|
|
175
|
+
"total_cost_usd": 0.012,
|
|
176
|
+
"total_requests": 5,
|
|
177
|
+
"by_model": {
|
|
178
|
+
"gpt-4o-mini": {"cost_usd": 0.0006, "requests": 2},
|
|
179
|
+
"gemini-2.5-flash": {"cost_usd": 0.0005, "requests": 1}
|
|
180
|
+
},
|
|
181
|
+
"by_user": {
|
|
182
|
+
"kyle@aucklanduni.ac.nz": {"cost_usd": 0.012, "requests": 5}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## 内置定价表
|
|
188
|
+
|
|
189
|
+
SDK 内置了常用模型的定价(USD / 1M tokens):
|
|
190
|
+
|
|
191
|
+
| 模型 | 输入价格 | 输出价格 |
|
|
192
|
+
|------|---------|---------|
|
|
193
|
+
| gpt-4o | $5.00 | $15.00 |
|
|
194
|
+
| gpt-4o-mini | $0.15 | $0.60 |
|
|
195
|
+
| o1 | $15.00 | $60.00 |
|
|
196
|
+
| o3-mini | $1.10 | $4.40 |
|
|
197
|
+
| gemini-2.5-pro | $1.25 | $10.00 |
|
|
198
|
+
| gemini-2.5-flash | $0.15 | $0.60 |
|
|
199
|
+
| claude-3-5-sonnet | $3.00 | $15.00 |
|
|
200
|
+
| claude-3-5-haiku | $0.80 | $4.00 |
|
|
201
|
+
|
|
202
|
+
自定义定价:
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
tracker.pricing.set("my-local-model", "ollama", input_per_1m=0.0, output_per_1m=0.0)
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
或通过 `.cost-tracker.yaml` 配置文件:
|
|
209
|
+
|
|
210
|
+
```yaml
|
|
211
|
+
project: DentalVLM
|
|
212
|
+
costs_dir: .cost-tracker
|
|
213
|
+
pricing:
|
|
214
|
+
my-local-model:
|
|
215
|
+
provider: ollama
|
|
216
|
+
input_per_1m: 0.0
|
|
217
|
+
output_per_1m: 0.0
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## 配置优先级
|
|
221
|
+
|
|
222
|
+
```
|
|
223
|
+
显式参数 > 环境变量 > .cost-tracker.yaml > 默认值
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
| 环境变量 | 说明 |
|
|
227
|
+
|----------|------|
|
|
228
|
+
| `COST_TRACKER_PROJECT` | 项目名称 |
|
|
229
|
+
| `COST_TRACKER_USER` | 用户邮箱 |
|
|
230
|
+
| `COST_TRACKER_REMOTE_URL` | 远端 API 地址 |
|
|
231
|
+
|
|
232
|
+
## 项目结构
|
|
233
|
+
|
|
234
|
+
```
|
|
235
|
+
sdk/
|
|
236
|
+
├── lab_cost_tracker/ # SDK 包(发布到 PyPI 的内容)
|
|
237
|
+
│ ├── __init__.py # 公共 API: CostTracker, PricingCatalog
|
|
238
|
+
│ ├── tracker.py # 主入口,wrap() / record() / summary()
|
|
239
|
+
│ ├── wrapper.py # 多 Provider 适配器(自动检测 + monkey-patch)
|
|
240
|
+
│ ├── pricing.py # 内置定价表 + 自定义覆盖
|
|
241
|
+
│ ├── storage.py # 本地双文件存储(detail.jsonl + summary.json)
|
|
242
|
+
│ ├── sync.py # 后台同步到远端 API(可选)
|
|
243
|
+
│ ├── config.py # YAML / env 配置加载
|
|
244
|
+
│ └── auth.py # Google OAuth 登录(可选)
|
|
245
|
+
├── tests/ # 自动化测试(Mock,不需要 API key)
|
|
246
|
+
│ └── test_all_providers.py
|
|
247
|
+
├── examples/ # 使用示例(需要 API key)
|
|
248
|
+
│ ├── openai_demo.py
|
|
249
|
+
│ ├── gemini_demo.py
|
|
250
|
+
│ └── proxy_demo.py
|
|
251
|
+
├── pyproject.toml # 包配置
|
|
252
|
+
├── README.md
|
|
253
|
+
└── LICENSE
|
|
254
|
+
```
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# Lab Cost Tracker SDK
|
|
2
|
+
|
|
3
|
+
轻量级 Python SDK,为研究组透明追踪 LLM API 调用费用。一行 `tracker.wrap()` 包裹现有 client,自动记录每次调用的模型、token 数和费用。
|
|
4
|
+
|
|
5
|
+
## 支持的 Provider
|
|
6
|
+
|
|
7
|
+
| Provider | Client 类型 | 拦截方法 |
|
|
8
|
+
|----------|------------|---------|
|
|
9
|
+
| OpenAI 官方 | `openai.OpenAI` | `chat.completions.create()` |
|
|
10
|
+
| Google Gemini | `google.genai.Client` | `models.generate_content()` |
|
|
11
|
+
| Anthropic | `anthropic.Anthropic` | `messages.create()` |
|
|
12
|
+
| 第三方代理 | `openai.OpenAI(base_url=...)` | 同 OpenAI(自动记录 `base_url`) |
|
|
13
|
+
|
|
14
|
+
## 安装
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
# 基础安装(仅需 pyyaml + requests)
|
|
18
|
+
pip install -e .
|
|
19
|
+
|
|
20
|
+
# 安装 OpenAI 支持
|
|
21
|
+
pip install -e ".[openai]"
|
|
22
|
+
|
|
23
|
+
# 安装 Google OAuth 支持(可选,用于身份验证)
|
|
24
|
+
pip install -e ".[google]"
|
|
25
|
+
|
|
26
|
+
# 全部安装
|
|
27
|
+
pip install -e ".[openai,google]"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## 快速开始
|
|
31
|
+
|
|
32
|
+
### 1. OpenAI
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from openai import OpenAI
|
|
36
|
+
from lab_cost_tracker import CostTracker
|
|
37
|
+
|
|
38
|
+
tracker = CostTracker(
|
|
39
|
+
project="DentalVLM",
|
|
40
|
+
user="kyle@aucklanduni.ac.nz",
|
|
41
|
+
storage_dir="./.cost-tracker", # 费用数据保存目录(默认值,可省略)
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
client = tracker.wrap(OpenAI(api_key="sk-..."))
|
|
45
|
+
|
|
46
|
+
# 正常使用,费用自动记录
|
|
47
|
+
response = client.chat.completions.create(
|
|
48
|
+
model="gpt-4o-mini",
|
|
49
|
+
messages=[{"role": "user", "content": "Hello!"}],
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
print(tracker.summary())
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### 2. Google Gemini
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from google import genai
|
|
59
|
+
from lab_cost_tracker import CostTracker
|
|
60
|
+
|
|
61
|
+
tracker = CostTracker(project="DentalVLM", user="kyle@aucklanduni.ac.nz")
|
|
62
|
+
|
|
63
|
+
client = tracker.wrap(genai.Client(api_key="..."))
|
|
64
|
+
|
|
65
|
+
response = client.models.generate_content(
|
|
66
|
+
model="gemini-2.5-flash",
|
|
67
|
+
contents="用一句话介绍新西兰奥克兰大学。",
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
print(tracker.summary())
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### 3. Anthropic
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
import anthropic
|
|
77
|
+
from lab_cost_tracker import CostTracker
|
|
78
|
+
|
|
79
|
+
tracker = CostTracker(project="DentalVLM", user="kyle@aucklanduni.ac.nz")
|
|
80
|
+
|
|
81
|
+
client = tracker.wrap(anthropic.Anthropic(api_key="..."))
|
|
82
|
+
|
|
83
|
+
response = client.messages.create(
|
|
84
|
+
model="claude-3-5-sonnet-20241022",
|
|
85
|
+
max_tokens=1024,
|
|
86
|
+
messages=[{"role": "user", "content": "Hello!"}],
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
print(tracker.summary())
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### 4. 第三方代理(如 apiyihe.org)
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from openai import OpenAI
|
|
96
|
+
from lab_cost_tracker import CostTracker
|
|
97
|
+
|
|
98
|
+
tracker = CostTracker(project="DentalVLM", user="kyle@aucklanduni.ac.nz")
|
|
99
|
+
|
|
100
|
+
# 使用 OpenAI SDK + 自定义 base_url
|
|
101
|
+
client = tracker.wrap(OpenAI(
|
|
102
|
+
api_key="...",
|
|
103
|
+
base_url="https://z.apiyihe.org/v1",
|
|
104
|
+
))
|
|
105
|
+
|
|
106
|
+
# 可以调用任何模型,base_url 会自动记录在 detail.jsonl 中
|
|
107
|
+
response = client.chat.completions.create(
|
|
108
|
+
model="gemini-2.5-flash-lite-preview-06-17",
|
|
109
|
+
messages=[{"role": "user", "content": "Hello!"}],
|
|
110
|
+
)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### 5. 手动记录(不支持的 Provider)
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
tracker.record(
|
|
117
|
+
model="llama-3-8b",
|
|
118
|
+
prompt_tokens=1000,
|
|
119
|
+
completion_tokens=500,
|
|
120
|
+
task="inference",
|
|
121
|
+
dataset="DentalBench",
|
|
122
|
+
)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## 数据存储
|
|
126
|
+
|
|
127
|
+
运行后会在 `storage_dir`(默认 `{项目根目录}/.cost-tracker/`)下生成两个文件:
|
|
128
|
+
|
|
129
|
+
### `detail.jsonl` — 逐条调用记录
|
|
130
|
+
|
|
131
|
+
```json
|
|
132
|
+
{"id": "42ae...", "timestamp": "2026-05-19T...", "project": "DentalVLM", "user": "kyle@aucklanduni.ac.nz", "model": "gpt-4o-mini-2024-07-18", "prompt_tokens": 19, "completion_tokens": 33, "cost_usd": 0.00059, "metadata": {"provider": "openai", "base_url": "https://api.openai.com/v1"}, "synced": false}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### `summary.json` — 聚合统计
|
|
136
|
+
|
|
137
|
+
```json
|
|
138
|
+
{
|
|
139
|
+
"project": "DentalVLM",
|
|
140
|
+
"total_cost_usd": 0.012,
|
|
141
|
+
"total_requests": 5,
|
|
142
|
+
"by_model": {
|
|
143
|
+
"gpt-4o-mini": {"cost_usd": 0.0006, "requests": 2},
|
|
144
|
+
"gemini-2.5-flash": {"cost_usd": 0.0005, "requests": 1}
|
|
145
|
+
},
|
|
146
|
+
"by_user": {
|
|
147
|
+
"kyle@aucklanduni.ac.nz": {"cost_usd": 0.012, "requests": 5}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## 内置定价表
|
|
153
|
+
|
|
154
|
+
SDK 内置了常用模型的定价(USD / 1M tokens):
|
|
155
|
+
|
|
156
|
+
| 模型 | 输入价格 | 输出价格 |
|
|
157
|
+
|------|---------|---------|
|
|
158
|
+
| gpt-4o | $5.00 | $15.00 |
|
|
159
|
+
| gpt-4o-mini | $0.15 | $0.60 |
|
|
160
|
+
| o1 | $15.00 | $60.00 |
|
|
161
|
+
| o3-mini | $1.10 | $4.40 |
|
|
162
|
+
| gemini-2.5-pro | $1.25 | $10.00 |
|
|
163
|
+
| gemini-2.5-flash | $0.15 | $0.60 |
|
|
164
|
+
| claude-3-5-sonnet | $3.00 | $15.00 |
|
|
165
|
+
| claude-3-5-haiku | $0.80 | $4.00 |
|
|
166
|
+
|
|
167
|
+
自定义定价:
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
tracker.pricing.set("my-local-model", "ollama", input_per_1m=0.0, output_per_1m=0.0)
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
或通过 `.cost-tracker.yaml` 配置文件:
|
|
174
|
+
|
|
175
|
+
```yaml
|
|
176
|
+
project: DentalVLM
|
|
177
|
+
costs_dir: .cost-tracker
|
|
178
|
+
pricing:
|
|
179
|
+
my-local-model:
|
|
180
|
+
provider: ollama
|
|
181
|
+
input_per_1m: 0.0
|
|
182
|
+
output_per_1m: 0.0
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## 配置优先级
|
|
186
|
+
|
|
187
|
+
```
|
|
188
|
+
显式参数 > 环境变量 > .cost-tracker.yaml > 默认值
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
| 环境变量 | 说明 |
|
|
192
|
+
|----------|------|
|
|
193
|
+
| `COST_TRACKER_PROJECT` | 项目名称 |
|
|
194
|
+
| `COST_TRACKER_USER` | 用户邮箱 |
|
|
195
|
+
| `COST_TRACKER_REMOTE_URL` | 远端 API 地址 |
|
|
196
|
+
|
|
197
|
+
## 项目结构
|
|
198
|
+
|
|
199
|
+
```
|
|
200
|
+
sdk/
|
|
201
|
+
├── lab_cost_tracker/ # SDK 包(发布到 PyPI 的内容)
|
|
202
|
+
│ ├── __init__.py # 公共 API: CostTracker, PricingCatalog
|
|
203
|
+
│ ├── tracker.py # 主入口,wrap() / record() / summary()
|
|
204
|
+
│ ├── wrapper.py # 多 Provider 适配器(自动检测 + monkey-patch)
|
|
205
|
+
│ ├── pricing.py # 内置定价表 + 自定义覆盖
|
|
206
|
+
│ ├── storage.py # 本地双文件存储(detail.jsonl + summary.json)
|
|
207
|
+
│ ├── sync.py # 后台同步到远端 API(可选)
|
|
208
|
+
│ ├── config.py # YAML / env 配置加载
|
|
209
|
+
│ └── auth.py # Google OAuth 登录(可选)
|
|
210
|
+
├── tests/ # 自动化测试(Mock,不需要 API key)
|
|
211
|
+
│ └── test_all_providers.py
|
|
212
|
+
├── examples/ # 使用示例(需要 API key)
|
|
213
|
+
│ ├── openai_demo.py
|
|
214
|
+
│ ├── gemini_demo.py
|
|
215
|
+
│ └── proxy_demo.py
|
|
216
|
+
├── pyproject.toml # 包配置
|
|
217
|
+
├── README.md
|
|
218
|
+
└── LICENSE
|
|
219
|
+
```
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Google OAuth authentication for lab members.
|
|
3
|
+
|
|
4
|
+
Uses Google OAuth 2.0 to authenticate users with their university email.
|
|
5
|
+
Tokens are cached locally to avoid repeated login prompts.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
_TOKEN_CACHE_DIR = Path.home() / ".config" / "lab-cost-tracker"
|
|
16
|
+
_TOKEN_CACHE_FILE = _TOKEN_CACHE_DIR / "credentials.json"
|
|
17
|
+
|
|
18
|
+
# Default OAuth client — users can override via env or config
|
|
19
|
+
_DEFAULT_CLIENT_ID = os.getenv("COST_TRACKER_GOOGLE_CLIENT_ID", "")
|
|
20
|
+
_DEFAULT_CLIENT_SECRET = os.getenv("COST_TRACKER_GOOGLE_CLIENT_SECRET", "")
|
|
21
|
+
_SCOPES = ["openid", "https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AuthInfo:
|
|
25
|
+
"""Authenticated user info."""
|
|
26
|
+
def __init__(self, email: str, name: str, id_token: str, access_token: str):
|
|
27
|
+
self.email = email
|
|
28
|
+
self.name = name
|
|
29
|
+
self.id_token = id_token
|
|
30
|
+
self.access_token = access_token
|
|
31
|
+
|
|
32
|
+
def to_dict(self) -> dict:
|
|
33
|
+
return {"email": self.email, "name": self.name}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def login(
|
|
37
|
+
client_id: Optional[str] = None,
|
|
38
|
+
client_secret: Optional[str] = None,
|
|
39
|
+
allowed_domains: Optional[list[str]] = None,
|
|
40
|
+
) -> AuthInfo:
|
|
41
|
+
"""
|
|
42
|
+
Perform Google OAuth login. Opens a browser for consent on first use,
|
|
43
|
+
then caches the refresh token for subsequent runs.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
client_id: Google OAuth client ID (or set COST_TRACKER_GOOGLE_CLIENT_ID)
|
|
47
|
+
client_secret: Google OAuth client secret (or set COST_TRACKER_GOOGLE_CLIENT_SECRET)
|
|
48
|
+
allowed_domains: If set, reject emails not matching these domains (e.g. ["aucklanduni.ac.nz"])
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
AuthInfo with email, name, and tokens.
|
|
52
|
+
"""
|
|
53
|
+
try:
|
|
54
|
+
from google_auth_oauthlib.flow import InstalledAppFlow
|
|
55
|
+
from google.oauth2.credentials import Credentials
|
|
56
|
+
from google.auth.transport.requests import Request
|
|
57
|
+
except ImportError:
|
|
58
|
+
raise ImportError(
|
|
59
|
+
"Google auth libraries required. Install with: "
|
|
60
|
+
"pip install google-auth google-auth-oauthlib"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
cid = client_id or _DEFAULT_CLIENT_ID
|
|
64
|
+
csecret = client_secret or _DEFAULT_CLIENT_SECRET
|
|
65
|
+
if not cid or not csecret:
|
|
66
|
+
raise ValueError(
|
|
67
|
+
"Google OAuth client_id and client_secret are required. "
|
|
68
|
+
"Set COST_TRACKER_GOOGLE_CLIENT_ID and COST_TRACKER_GOOGLE_CLIENT_SECRET "
|
|
69
|
+
"environment variables, or pass them directly."
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
creds = _load_cached_credentials()
|
|
73
|
+
|
|
74
|
+
# Try to refresh existing credentials
|
|
75
|
+
if creds and creds.expired and creds.refresh_token:
|
|
76
|
+
try:
|
|
77
|
+
creds.refresh(Request())
|
|
78
|
+
except Exception:
|
|
79
|
+
creds = None
|
|
80
|
+
|
|
81
|
+
# No valid credentials — run OAuth flow
|
|
82
|
+
if not creds or not creds.valid:
|
|
83
|
+
client_config = {
|
|
84
|
+
"installed": {
|
|
85
|
+
"client_id": cid,
|
|
86
|
+
"client_secret": csecret,
|
|
87
|
+
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
|
88
|
+
"token_uri": "https://oauth2.googleapis.com/token",
|
|
89
|
+
"redirect_uris": ["http://localhost"],
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
flow = InstalledAppFlow.from_client_config(client_config, _SCOPES)
|
|
93
|
+
creds = flow.run_local_server(port=0, prompt="consent")
|
|
94
|
+
_save_cached_credentials(creds)
|
|
95
|
+
|
|
96
|
+
# Get user info
|
|
97
|
+
import requests as req
|
|
98
|
+
resp = req.get(
|
|
99
|
+
"https://www.googleapis.com/oauth2/v3/userinfo",
|
|
100
|
+
headers={"Authorization": f"Bearer {creds.token}"},
|
|
101
|
+
)
|
|
102
|
+
resp.raise_for_status()
|
|
103
|
+
user_info = resp.json()
|
|
104
|
+
|
|
105
|
+
email = user_info.get("email", "")
|
|
106
|
+
name = user_info.get("name", email.split("@")[0])
|
|
107
|
+
|
|
108
|
+
# Domain check
|
|
109
|
+
if allowed_domains:
|
|
110
|
+
domain = email.split("@")[-1] if "@" in email else ""
|
|
111
|
+
if domain not in allowed_domains:
|
|
112
|
+
raise PermissionError(
|
|
113
|
+
f"Email domain '{domain}' is not allowed. "
|
|
114
|
+
f"Allowed domains: {allowed_domains}"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
return AuthInfo(
|
|
118
|
+
email=email,
|
|
119
|
+
name=name,
|
|
120
|
+
id_token=creds.token,
|
|
121
|
+
access_token=creds.token,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _load_cached_credentials():
|
|
126
|
+
"""Load cached Google OAuth credentials from disk."""
|
|
127
|
+
try:
|
|
128
|
+
from google.oauth2.credentials import Credentials
|
|
129
|
+
except ImportError:
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
if not _TOKEN_CACHE_FILE.exists():
|
|
133
|
+
return None
|
|
134
|
+
try:
|
|
135
|
+
with open(_TOKEN_CACHE_FILE, "r") as f:
|
|
136
|
+
data = json.load(f)
|
|
137
|
+
return Credentials(
|
|
138
|
+
token=data.get("token"),
|
|
139
|
+
refresh_token=data.get("refresh_token"),
|
|
140
|
+
token_uri=data.get("token_uri", "https://oauth2.googleapis.com/token"),
|
|
141
|
+
client_id=data.get("client_id"),
|
|
142
|
+
client_secret=data.get("client_secret"),
|
|
143
|
+
scopes=data.get("scopes"),
|
|
144
|
+
)
|
|
145
|
+
except Exception:
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _save_cached_credentials(creds):
|
|
150
|
+
"""Cache credentials to disk for reuse."""
|
|
151
|
+
_TOKEN_CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
|
152
|
+
data = {
|
|
153
|
+
"token": creds.token,
|
|
154
|
+
"refresh_token": creds.refresh_token,
|
|
155
|
+
"token_uri": creds.token_uri,
|
|
156
|
+
"client_id": creds.client_id,
|
|
157
|
+
"client_secret": creds.client_secret,
|
|
158
|
+
"scopes": list(creds.scopes) if creds.scopes else _SCOPES,
|
|
159
|
+
}
|
|
160
|
+
with open(_TOKEN_CACHE_FILE, "w") as f:
|
|
161
|
+
json.dump(data, f, indent=2)
|
|
162
|
+
os.chmod(_TOKEN_CACHE_FILE, 0o600) # restrict permissions
|