tokenkeeper-ai 0.2.0__py3-none-any.whl
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.
- tokenkeeper/__init__.py +157 -0
- tokenkeeper/cli.py +84 -0
- tokenkeeper/core.py +360 -0
- tokenkeeper/guard.py +390 -0
- tokenkeeper/integrations/__init__.py +8 -0
- tokenkeeper/integrations/anthropic.py +480 -0
- tokenkeeper/integrations/openai_compat.py +479 -0
- tokenkeeper/ledger.py +712 -0
- tokenkeeper/pricing.py +628 -0
- tokenkeeper/pricing_data.py +311 -0
- tokenkeeper_ai-0.2.0.dist-info/METADATA +539 -0
- tokenkeeper_ai-0.2.0.dist-info/RECORD +16 -0
- tokenkeeper_ai-0.2.0.dist-info/WHEEL +5 -0
- tokenkeeper_ai-0.2.0.dist-info/entry_points.txt +2 -0
- tokenkeeper_ai-0.2.0.dist-info/licenses/LICENSE +21 -0
- tokenkeeper_ai-0.2.0.dist-info/top_level.txt +1 -0
tokenkeeper/__init__.py
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""tokenkeeper — AI API 成本监控与限流守护者。
|
|
2
|
+
|
|
3
|
+
Quickstart(5 行接入)::
|
|
4
|
+
|
|
5
|
+
from tokenkeeper import guard
|
|
6
|
+
|
|
7
|
+
guard.install(project="my-app", user="alice")
|
|
8
|
+
|
|
9
|
+
# 你原来的代码一行不改——所有 openai 调用自动记账 + 限额保护
|
|
10
|
+
|
|
11
|
+
启动看板::
|
|
12
|
+
|
|
13
|
+
$ tokenkeeper dashboard
|
|
14
|
+
# → http://localhost:8501
|
|
15
|
+
|
|
16
|
+
直接查询账本::
|
|
17
|
+
|
|
18
|
+
from tokenkeeper import ledger
|
|
19
|
+
|
|
20
|
+
calls = ledger.query(since=time.time() - 86400) # 最近 24 小时
|
|
21
|
+
total_usd, total_cny = ledger.total_cost(since=time.time() - 86400)
|
|
22
|
+
|
|
23
|
+
详细文档见各子模块:
|
|
24
|
+
- :mod:`tokenkeeper.pricing` — 模型价格表与成本计算
|
|
25
|
+
- :mod:`tokenkeeper.ledger` — SQLite 账本
|
|
26
|
+
- :mod:`tokenkeeper.guard` — 限额熔断
|
|
27
|
+
- :mod:`tokenkeeper.core` — 拦截核心
|
|
28
|
+
- :mod:`tokenkeeper.integrations.openai_compat` — OpenAI 兼容协议
|
|
29
|
+
|
|
30
|
+
设计原则(docs/PROJECT_PLAN.md):
|
|
31
|
+
- 零侵入接入(monkey-patch)
|
|
32
|
+
- 数据自产(dogfooding)
|
|
33
|
+
- 本地优先(SQLite + 本地文件)
|
|
34
|
+
- 国内友好(覆盖国产模型)
|
|
35
|
+
- MIT 开源
|
|
36
|
+
|
|
37
|
+
异常层级:
|
|
38
|
+
- :class:`tokenkeeper.ledger.LedgerError` — 账本相关错误
|
|
39
|
+
- :class:`tokenkeeper.pricing.PricingConfigError` — 价格配置错误
|
|
40
|
+
- :class:`tokenkeeper.guard.BudgetExceededError` — 预算超限
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
from __future__ import annotations
|
|
44
|
+
|
|
45
|
+
# 版本号遵循语义化版本(semver.org)
|
|
46
|
+
__version__ = "0.1.0"
|
|
47
|
+
__author__ = "tokenkeeper contributors"
|
|
48
|
+
|
|
49
|
+
__all__ = [
|
|
50
|
+
"__version__",
|
|
51
|
+
# 高级 API(用户直接用)
|
|
52
|
+
"guard",
|
|
53
|
+
"ledger",
|
|
54
|
+
"dashboard",
|
|
55
|
+
# 核心数据类(用户可能用)
|
|
56
|
+
"CallRecord",
|
|
57
|
+
"ModelPricing",
|
|
58
|
+
"CostBreakdown",
|
|
59
|
+
# 异常
|
|
60
|
+
"LedgerError",
|
|
61
|
+
"PricingConfigError",
|
|
62
|
+
"BudgetExceededError",
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# ====================================================================
|
|
67
|
+
# 延迟加载(避免循环引用 + 加快 import 速度)
|
|
68
|
+
# ====================================================================
|
|
69
|
+
|
|
70
|
+
# 价格表(无依赖,第一个加载)
|
|
71
|
+
from . import pricing # noqa: E402
|
|
72
|
+
from .pricing import ( # noqa: E402,F401
|
|
73
|
+
ModelPricing,
|
|
74
|
+
CostBreakdown,
|
|
75
|
+
PricingConfigError,
|
|
76
|
+
calculate_cost,
|
|
77
|
+
get_pricing,
|
|
78
|
+
list_models,
|
|
79
|
+
register_custom_pricing,
|
|
80
|
+
USD_TO_CNY,
|
|
81
|
+
PRICING_LAST_UPDATED,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# 账本(依赖 sqlite3)
|
|
86
|
+
from . import ledger # noqa: E402
|
|
87
|
+
from .ledger import ( # noqa: E402,F401
|
|
88
|
+
CallRecord,
|
|
89
|
+
Ledger,
|
|
90
|
+
LedgerError,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# guard 模块自身定义 Budget / Guard / GuardDecision / BudgetExceededError
|
|
95
|
+
# 顶级 guard 单例在 core.py
|
|
96
|
+
from . import guard as _guard_module # noqa: E402,F401
|
|
97
|
+
from .guard import ( # noqa: E402,F401
|
|
98
|
+
Budget,
|
|
99
|
+
Guard,
|
|
100
|
+
GuardDecision,
|
|
101
|
+
BudgetExceededError,
|
|
102
|
+
BUDGET_CACHE_TTL_SECONDS,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# 顶级 guard 单例(覆盖 guard 模块名)
|
|
106
|
+
from .core import guard # noqa: E402,F401
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# 拦截核心(依赖 guard)
|
|
110
|
+
# 注意:core 暴露顶级 API(guard.install 实际指向 core.install)
|
|
111
|
+
from . import core # noqa: E402,F401
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# 集成(可选,依赖 core)
|
|
115
|
+
# 注意:导入这个会自动 patch OpenAI SDK
|
|
116
|
+
# 所以默认不导入,用户需要时显式 from tokenkeeper.integrations import openai_compat
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# 看板(可选,依赖 streamlit)
|
|
120
|
+
# 注意:依赖 streamlit,未安装时会报错
|
|
121
|
+
# 用户运行 `tokenkeeper dashboard` 时才会触发
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
__all__ = sorted(set(__all__ + pricing.__all__ + ledger.__all__ + _guard_module.__all__ + core.__all__))
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# 模块信息(方便调试)
|
|
128
|
+
def _info() -> dict:
|
|
129
|
+
"""返回 tokenkeeper 运行时信息(用于调试和健康检查)。"""
|
|
130
|
+
return {
|
|
131
|
+
"version": __version__,
|
|
132
|
+
"pricing_models": len(pricing.list_models()),
|
|
133
|
+
"pricing_last_updated": pricing.PRICING_LAST_UPDATED,
|
|
134
|
+
"pricing_overrides": len(pricing._CUSTOM_PRICING),
|
|
135
|
+
"exchange_rate_usd_cny": pricing.get_exchange_rate(),
|
|
136
|
+
"modules_loaded": ["pricing", "ledger", "guard", "core"],
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# 启动横幅(导入时打印)
|
|
141
|
+
def _banner() -> str:
|
|
142
|
+
"""生成 tokenkeeper 启动横幅(ASCII art)。"""
|
|
143
|
+
return f"""
|
|
144
|
+
╭─────────────────────────────────────────╮
|
|
145
|
+
│ 🪶 tokenkeeper v{__version__:<10s} │
|
|
146
|
+
│ AI API 成本监控与限流守护者 │
|
|
147
|
+
│ {len(pricing.list_models())} 个内置模型 | 价格更新 {pricing.PRICING_LAST_UPDATED} │
|
|
148
|
+
╰─────────────────────────────────────────╯
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
if __name__ == "__main__":
|
|
153
|
+
# 模块自检
|
|
154
|
+
import json
|
|
155
|
+
print(_banner())
|
|
156
|
+
print("运行时信息:")
|
|
157
|
+
print(json.dumps(_info(), ensure_ascii=False, indent=2))
|
tokenkeeper/cli.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""tokenkeeper 命令行入口。
|
|
2
|
+
|
|
3
|
+
命令:
|
|
4
|
+
- tokenkeeper dashboard 启动 Streamlit 看板
|
|
5
|
+
- tokenkeeper version 显示版本
|
|
6
|
+
- tokenkeeper info 显示运行时信息
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
from . import __version__, _info
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def main() -> None:
|
|
18
|
+
"""tokenkeeper CLI 主入口。"""
|
|
19
|
+
parser = argparse.ArgumentParser(
|
|
20
|
+
prog="tokenkeeper",
|
|
21
|
+
description="AI API 成本监控与限流守护者",
|
|
22
|
+
)
|
|
23
|
+
subparsers = parser.add_subparsers(dest="command", help="子命令")
|
|
24
|
+
|
|
25
|
+
# version
|
|
26
|
+
subparsers.add_parser("version", help="显示版本")
|
|
27
|
+
|
|
28
|
+
# info
|
|
29
|
+
subparsers.add_parser("info", help="显示运行时信息")
|
|
30
|
+
|
|
31
|
+
# dashboard
|
|
32
|
+
dash_parser = subparsers.add_parser("dashboard", help="启动 Streamlit 看板")
|
|
33
|
+
dash_parser.add_argument(
|
|
34
|
+
"--port", type=int, default=8501,
|
|
35
|
+
help="端口(默认 8501)",
|
|
36
|
+
)
|
|
37
|
+
dash_parser.add_argument(
|
|
38
|
+
"--db", default="./tokenkeeper.db",
|
|
39
|
+
help="SQLite DB 路径(默认 ./tokenkeeper.db)",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
args = parser.parse_args()
|
|
43
|
+
|
|
44
|
+
if args.command == "version":
|
|
45
|
+
print(f"tokenkeeper {__version__}")
|
|
46
|
+
elif args.command == "info":
|
|
47
|
+
import json
|
|
48
|
+
info = _info()
|
|
49
|
+
info["db_default"] = "./tokenkeeper.db"
|
|
50
|
+
print(json.dumps(info, ensure_ascii=False, indent=2))
|
|
51
|
+
elif args.command == "dashboard":
|
|
52
|
+
run_dashboard(port=args.port, db=args.db)
|
|
53
|
+
else:
|
|
54
|
+
parser.print_help()
|
|
55
|
+
sys.exit(1)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def run_dashboard(port: int, db: str) -> None:
|
|
59
|
+
"""启动 Streamlit 看板。"""
|
|
60
|
+
try:
|
|
61
|
+
import streamlit.web.cli as stcli
|
|
62
|
+
except ImportError:
|
|
63
|
+
print(
|
|
64
|
+
"❌ streamlit 未安装。\n"
|
|
65
|
+
"请运行: pip install tokenkeeper[dashboard]"
|
|
66
|
+
)
|
|
67
|
+
sys.exit(1)
|
|
68
|
+
|
|
69
|
+
import os
|
|
70
|
+
dashboard_path = os.path.join(
|
|
71
|
+
os.path.dirname(__file__),
|
|
72
|
+
"dashboard",
|
|
73
|
+
"app.py",
|
|
74
|
+
)
|
|
75
|
+
if not os.path.exists(dashboard_path):
|
|
76
|
+
print(f"❌ 看板文件不存在: {dashboard_path}")
|
|
77
|
+
sys.exit(1)
|
|
78
|
+
|
|
79
|
+
sys.argv = ["streamlit", "run", dashboard_path, "--server.port", str(port)]
|
|
80
|
+
stcli.main()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
if __name__ == "__main__":
|
|
84
|
+
main()
|
tokenkeeper/core.py
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
"""tokenkeeper.core — 拦截核心与顶级 guard API。
|
|
2
|
+
|
|
3
|
+
模块职责(架构师定稿,2026-06-23):
|
|
4
|
+
1. 提供顶级 ``guard`` API(``guard.install()`` / ``guard.uninstall()``)
|
|
5
|
+
2. 协调 ledger + guard + 各种 integrations
|
|
6
|
+
3. 维护"已 patch 的 SDK"状态,防止重复 patch
|
|
7
|
+
4. 提供上下文管理器(``guard.temporarily_disabled()``)
|
|
8
|
+
|
|
9
|
+
公开 API(__all__):
|
|
10
|
+
- guard (顶级单例)
|
|
11
|
+
- GuardAPI (类)
|
|
12
|
+
|
|
13
|
+
设计:
|
|
14
|
+
- ``guard`` 是全局单例,第一次 ``from tokenkeeper import guard`` 时创建
|
|
15
|
+
- ``guard.install()`` 一次性初始化 ledger + 注册拦截器
|
|
16
|
+
- ``guard.uninstall()`` 恢复原始 SDK 方法
|
|
17
|
+
- 重复 install() 是幂等的(不会重复 patch)
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import logging
|
|
23
|
+
import threading
|
|
24
|
+
from contextlib import contextmanager
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Iterator, Optional
|
|
27
|
+
|
|
28
|
+
from .guard import Budget, BudgetExceededError, Guard, GuardDecision
|
|
29
|
+
from .ledger import Ledger
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"guard",
|
|
33
|
+
"GuardAPI",
|
|
34
|
+
"Budget",
|
|
35
|
+
"BudgetExceededError",
|
|
36
|
+
"GuardDecision",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
logger = logging.getLogger(__name__)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ====================================================================
|
|
44
|
+
# 顶级 guard API
|
|
45
|
+
# ====================================================================
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class GuardAPI:
|
|
49
|
+
"""tokenkeeper 顶级 API(用户用 ``from tokenkeeper import guard``)。
|
|
50
|
+
|
|
51
|
+
这是单例(顶层只有一个 guard 实例)。线程安全。
|
|
52
|
+
|
|
53
|
+
Examples:
|
|
54
|
+
>>> from tokenkeeper import guard
|
|
55
|
+
>>> guard.install(project="my-app", db_path="./tk.db")
|
|
56
|
+
>>> guard.set_budget(daily_limit_usd=10.0, action="block")
|
|
57
|
+
>>> # 业务代码 0 改动,openai 调用自动记账 + 限额
|
|
58
|
+
>>> guard.uninstall()
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
def __init__(self) -> None:
|
|
62
|
+
self._lock = threading.Lock()
|
|
63
|
+
self._ledger: Optional[Ledger] = None
|
|
64
|
+
self._guard: Optional[Guard] = None
|
|
65
|
+
self._installed: bool = False
|
|
66
|
+
self._project: str = "default"
|
|
67
|
+
self._user: str = "default"
|
|
68
|
+
self._patched_sdks: list[str] = [] # 跟踪已 patch 的 SDK
|
|
69
|
+
|
|
70
|
+
# ------------------------------------------------------------------
|
|
71
|
+
# 安装 / 卸载
|
|
72
|
+
# ------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
def install(
|
|
75
|
+
self,
|
|
76
|
+
db_path: str | Path = "./tokenkeeper.db",
|
|
77
|
+
project: str = "default",
|
|
78
|
+
user: str = "default",
|
|
79
|
+
auto_patch_openai: bool = True,
|
|
80
|
+
) -> None:
|
|
81
|
+
"""安装 tokenkeeper。
|
|
82
|
+
|
|
83
|
+
- 创建/打开 ledger
|
|
84
|
+
- 创建 guard
|
|
85
|
+
- (可选)自动 patch OpenAI SDK
|
|
86
|
+
|
|
87
|
+
重复调用是幂等的。
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
db_path: SQLite 文件路径
|
|
91
|
+
project: 项目标识
|
|
92
|
+
user: 用户标识
|
|
93
|
+
auto_patch_openai: 是否自动 patch OpenAI SDK(默认 True)
|
|
94
|
+
"""
|
|
95
|
+
with self._lock:
|
|
96
|
+
if self._installed:
|
|
97
|
+
logger.debug("guard.install() 重复调用,已安装,跳过")
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
# 1. 创建 ledger
|
|
101
|
+
self._ledger = Ledger(db_path)
|
|
102
|
+
self._project = project
|
|
103
|
+
self._user = user
|
|
104
|
+
|
|
105
|
+
# 2. 创建 guard
|
|
106
|
+
self._guard = Guard(self._ledger)
|
|
107
|
+
|
|
108
|
+
# 3. (可选)自动 patch OpenAI SDK
|
|
109
|
+
if auto_patch_openai:
|
|
110
|
+
try:
|
|
111
|
+
self._patch_openai()
|
|
112
|
+
except ImportError:
|
|
113
|
+
logger.warning(
|
|
114
|
+
"openai 包未安装,跳过 patch。"
|
|
115
|
+
"用户调用 openai 时不会被 tokenkeeper 拦截。"
|
|
116
|
+
)
|
|
117
|
+
except Exception as e:
|
|
118
|
+
logger.error("patch SDK 失败,但 tokenkeeper 仍可使用: %s", e)
|
|
119
|
+
|
|
120
|
+
# 确保即使 patch 失败,tokenkeeper 也算安装成功
|
|
121
|
+
try:
|
|
122
|
+
self._installed = True
|
|
123
|
+
logger.info(
|
|
124
|
+
"tokenkeeper 已安装: db=%s project=%s user=%s",
|
|
125
|
+
db_path, project, user,
|
|
126
|
+
)
|
|
127
|
+
except Exception as e:
|
|
128
|
+
logger.error("设置 _installed 失败: %s", e)
|
|
129
|
+
raise
|
|
130
|
+
raise
|
|
131
|
+
|
|
132
|
+
def uninstall(self) -> None:
|
|
133
|
+
"""卸载 tokenkeeper,恢复原始 SDK。"""
|
|
134
|
+
with self._lock:
|
|
135
|
+
if not self._installed:
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
# 恢复 SDK
|
|
139
|
+
for sdk_name in self._patched_sdks:
|
|
140
|
+
try:
|
|
141
|
+
self._unpatch_sdk(sdk_name)
|
|
142
|
+
except Exception as e:
|
|
143
|
+
logger.error("unpatch %s 失败: %s", sdk_name, e)
|
|
144
|
+
self._patched_sdks.clear()
|
|
145
|
+
|
|
146
|
+
# 关闭 ledger
|
|
147
|
+
if self._ledger is not None:
|
|
148
|
+
self._ledger.close()
|
|
149
|
+
|
|
150
|
+
self._ledger = None
|
|
151
|
+
self._guard = None
|
|
152
|
+
self._installed = False
|
|
153
|
+
logger.info("tokenkeeper 已卸载")
|
|
154
|
+
|
|
155
|
+
@contextmanager
|
|
156
|
+
def temporarily_disabled(self) -> Iterator[None]:
|
|
157
|
+
"""上下文管理器:临时关闭拦截(用于性能关键路径)。
|
|
158
|
+
|
|
159
|
+
Examples:
|
|
160
|
+
>>> with guard.temporarily_disabled():
|
|
161
|
+
... # 这里的 openai 调用不会被记账
|
|
162
|
+
... client.chat.completions.create(...)
|
|
163
|
+
"""
|
|
164
|
+
was_installed = self._installed
|
|
165
|
+
if was_installed:
|
|
166
|
+
self.uninstall()
|
|
167
|
+
try:
|
|
168
|
+
yield
|
|
169
|
+
finally:
|
|
170
|
+
if was_installed:
|
|
171
|
+
self.install(
|
|
172
|
+
db_path=str(self._ledger.db_path) if self._ledger else "./tokenkeeper.db",
|
|
173
|
+
project=self._project,
|
|
174
|
+
user=self._user,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# ------------------------------------------------------------------
|
|
178
|
+
# 预算管理(代理到内部 _guard)
|
|
179
|
+
# ------------------------------------------------------------------
|
|
180
|
+
|
|
181
|
+
def set_budget(
|
|
182
|
+
self,
|
|
183
|
+
daily_limit_usd: Optional[float] = None,
|
|
184
|
+
monthly_limit_usd: Optional[float] = None,
|
|
185
|
+
per_call_limit_usd: Optional[float] = None,
|
|
186
|
+
action: str = "block",
|
|
187
|
+
scope: str = "global",
|
|
188
|
+
scope_key: Optional[str] = None,
|
|
189
|
+
) -> None:
|
|
190
|
+
"""设置预算(快捷方法)。
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
daily_limit_usd: 日预算
|
|
194
|
+
monthly_limit_usd: 月预算
|
|
195
|
+
per_call_limit_usd: 单次预算
|
|
196
|
+
action: 超限动作("block" / "warn")
|
|
197
|
+
scope: 范围("global" / "project" / "user")
|
|
198
|
+
scope_key: scope 为 project/user 时必填
|
|
199
|
+
"""
|
|
200
|
+
if not self._installed:
|
|
201
|
+
raise RuntimeError("请先调用 guard.install()")
|
|
202
|
+
budget = Budget(
|
|
203
|
+
scope=scope,
|
|
204
|
+
scope_key=scope_key,
|
|
205
|
+
daily_limit_usd=daily_limit_usd,
|
|
206
|
+
monthly_limit_usd=monthly_limit_usd,
|
|
207
|
+
per_call_limit_usd=per_call_limit_usd,
|
|
208
|
+
action=action,
|
|
209
|
+
)
|
|
210
|
+
self._guard.set_budget(budget)
|
|
211
|
+
|
|
212
|
+
def clear_budgets(self) -> None:
|
|
213
|
+
"""清空所有预算。"""
|
|
214
|
+
if not self._installed:
|
|
215
|
+
return
|
|
216
|
+
self._guard.clear_budgets()
|
|
217
|
+
|
|
218
|
+
# ------------------------------------------------------------------
|
|
219
|
+
# 直接记账(用户手动调用)
|
|
220
|
+
# ------------------------------------------------------------------
|
|
221
|
+
|
|
222
|
+
def record(
|
|
223
|
+
self,
|
|
224
|
+
model: str,
|
|
225
|
+
prompt_tokens: int = 0,
|
|
226
|
+
completion_tokens: int = 0,
|
|
227
|
+
cost_usd: float = 0.0,
|
|
228
|
+
cost_cny: float = 0.0,
|
|
229
|
+
latency_ms: float = 0.0,
|
|
230
|
+
status: str = "success",
|
|
231
|
+
error: Optional[str] = None,
|
|
232
|
+
provider: str = "unknown",
|
|
233
|
+
) -> Optional[int]:
|
|
234
|
+
"""手动记录一次调用(用于非 OpenAI SDK 的场景)。
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
新插入的 rowid,失败 None
|
|
238
|
+
"""
|
|
239
|
+
if not self._installed or self._ledger is None:
|
|
240
|
+
logger.error("guard 未安装,无法记录")
|
|
241
|
+
return None
|
|
242
|
+
|
|
243
|
+
from .ledger import CallRecord
|
|
244
|
+
import time
|
|
245
|
+
|
|
246
|
+
record = CallRecord(
|
|
247
|
+
timestamp=time.time(),
|
|
248
|
+
project=self._project,
|
|
249
|
+
user=self._user,
|
|
250
|
+
provider=provider,
|
|
251
|
+
model=model,
|
|
252
|
+
prompt_tokens=prompt_tokens,
|
|
253
|
+
completion_tokens=completion_tokens,
|
|
254
|
+
cost_usd=cost_usd,
|
|
255
|
+
cost_cny=cost_cny,
|
|
256
|
+
latency_ms=latency_ms,
|
|
257
|
+
status=status,
|
|
258
|
+
error=error,
|
|
259
|
+
)
|
|
260
|
+
return self._ledger.record(record)
|
|
261
|
+
|
|
262
|
+
# ------------------------------------------------------------------
|
|
263
|
+
# 查询(代理到 ledger)
|
|
264
|
+
# ------------------------------------------------------------------
|
|
265
|
+
|
|
266
|
+
def query(self, **kwargs):
|
|
267
|
+
"""查询调用记录(代理到 ledger)。"""
|
|
268
|
+
if not self._installed or self._ledger is None:
|
|
269
|
+
return []
|
|
270
|
+
return self._ledger.query(**kwargs)
|
|
271
|
+
|
|
272
|
+
def summary(self, **kwargs):
|
|
273
|
+
"""汇总(代理到 ledger)。"""
|
|
274
|
+
if not self._installed or self._ledger is None:
|
|
275
|
+
return []
|
|
276
|
+
return self._ledger.summary(**kwargs)
|
|
277
|
+
|
|
278
|
+
def total_cost(self, **kwargs) -> tuple[float, float]:
|
|
279
|
+
"""总成本(代理到 ledger)。"""
|
|
280
|
+
if not self._installed or self._ledger is None:
|
|
281
|
+
return 0.0, 0.0
|
|
282
|
+
return self._ledger.total_cost(**kwargs)
|
|
283
|
+
|
|
284
|
+
# ------------------------------------------------------------------
|
|
285
|
+
# SDK Patch 管理
|
|
286
|
+
# ------------------------------------------------------------------
|
|
287
|
+
|
|
288
|
+
def _patch_openai(self) -> bool:
|
|
289
|
+
"""patch OpenAI 和 Anthropic SDK(延迟 import)。
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
bool: 是否成功 patch 至少一个 SDK
|
|
293
|
+
"""
|
|
294
|
+
success = False
|
|
295
|
+
|
|
296
|
+
try:
|
|
297
|
+
# OpenAI 兼容协议
|
|
298
|
+
try:
|
|
299
|
+
from .integrations.openai_compat import install as install_openai
|
|
300
|
+
install_openai(self)
|
|
301
|
+
self._patched_sdks.append("openai")
|
|
302
|
+
logger.info("OpenAI SDK 已 patch")
|
|
303
|
+
success = True
|
|
304
|
+
except ImportError as e:
|
|
305
|
+
logger.warning("无法 import openai_compat: %s", e)
|
|
306
|
+
except Exception as e:
|
|
307
|
+
logger.error("patch OpenAI 失败: %s", e)
|
|
308
|
+
|
|
309
|
+
# Anthropic 原生 SDK
|
|
310
|
+
try:
|
|
311
|
+
from .integrations.anthropic import install as install_anthropic
|
|
312
|
+
install_anthropic(self)
|
|
313
|
+
self._patched_sdks.append("anthropic")
|
|
314
|
+
logger.info("Anthropic SDK 已 patch")
|
|
315
|
+
success = True
|
|
316
|
+
except ImportError as e:
|
|
317
|
+
logger.warning("无法 import anthropic: %s", e)
|
|
318
|
+
except Exception as e:
|
|
319
|
+
logger.error("patch Anthropic 失败: %s", e)
|
|
320
|
+
# 不影响 _installed 状态,只是 Anthropic 不记账
|
|
321
|
+
except Exception as e:
|
|
322
|
+
logger.error("patch SDK 过程中发生未预期错误: %s", e)
|
|
323
|
+
|
|
324
|
+
return success
|
|
325
|
+
def _unpatch_sdk(self, sdk_name: str) -> None:
|
|
326
|
+
"""unpatch SDK。"""
|
|
327
|
+
if sdk_name == "openai":
|
|
328
|
+
try:
|
|
329
|
+
from .integrations.openai_compat import uninstall as uninstall_openai
|
|
330
|
+
uninstall_openai()
|
|
331
|
+
logger.info("OpenAI SDK 已 unpatch")
|
|
332
|
+
except ImportError:
|
|
333
|
+
pass
|
|
334
|
+
elif sdk_name == "anthropic":
|
|
335
|
+
try:
|
|
336
|
+
from .integrations.anthropic import uninstall as uninstall_anthropic
|
|
337
|
+
uninstall_anthropic()
|
|
338
|
+
logger.info("Anthropic SDK 已 unpatch")
|
|
339
|
+
except ImportError:
|
|
340
|
+
pass
|
|
341
|
+
|
|
342
|
+
# ------------------------------------------------------------------
|
|
343
|
+
# 状态查询
|
|
344
|
+
# ------------------------------------------------------------------
|
|
345
|
+
|
|
346
|
+
def is_installed(self) -> bool:
|
|
347
|
+
"""是否已安装。"""
|
|
348
|
+
return self._installed
|
|
349
|
+
|
|
350
|
+
def ledger(self) -> Optional[Ledger]:
|
|
351
|
+
"""获取内部 ledger 实例(高级用户用)。"""
|
|
352
|
+
return self._ledger
|
|
353
|
+
|
|
354
|
+
def guard_instance(self) -> Optional[Guard]:
|
|
355
|
+
"""获取内部 guard 实例(高级用户用)。"""
|
|
356
|
+
return self._guard
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
# 全局单例
|
|
360
|
+
guard = GuardAPI()
|