fix: 限额改 v4 冻结快照,余额红线防扣费
v2 的 stable_limit = usage + balance - BUFFER 依赖余额 API 随消耗实时递减, 实测滞后(08-12 快照 1,558,307 在 usage 涨 45 万期间不动)导致池膨胀、限额 虚高,放行到扣费区(当日超余额 13.8 万已扣费)。 扣费口径(用户确认):是否扣费只取决于当下免费额度,超出即扣费,采集量只 决定明日发放。v4:stable_limit = balance - BUFFER 冻结快照,本地 usage 实时 撞线即拦,等价于本地实时扣减余额;effective = min(2M 配额, stable_limit)。 新增 docs/volc-quota-notes.md 额度机制速查;merge-auto-consume.md 补 §4.5。
This commit is contained in:
@@ -0,0 +1,341 @@
|
||||
import ast
|
||||
import hashlib
|
||||
import os
|
||||
import re
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import yaml
|
||||
|
||||
# ==========================================
|
||||
# 🛠️ 配置
|
||||
# ==========================================
|
||||
CONFIG_FILE = "config.yaml"
|
||||
ENV_FILE = ".env"
|
||||
RUNTIME_FILE = "volc_limit_sync.py"
|
||||
|
||||
|
||||
def load_env(env_path):
|
||||
"""读取 .env 文件并返回字典"""
|
||||
env_vars = {}
|
||||
if not os.path.exists(env_path):
|
||||
print(f"⚠️ Warning: {env_path} not found. Env var substitution might fail.")
|
||||
return env_vars
|
||||
|
||||
with open(env_path, "r", encoding="utf-8") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
try:
|
||||
key, value = line.split("=", 1)
|
||||
env_vars[key.strip()] = value.strip().strip('"').strip("'")
|
||||
except ValueError:
|
||||
continue
|
||||
return env_vars
|
||||
|
||||
|
||||
def resolve_env_var(value, env_vars):
|
||||
"""
|
||||
处理环境变量替换,支持以下两种格式:
|
||||
1. ${VAR_NAME} - 标准格式
|
||||
2. os.environ/VAR_NAME - LiteLLM 特有格式
|
||||
"""
|
||||
if not isinstance(value, str):
|
||||
return value
|
||||
|
||||
if value.startswith("os.environ/"):
|
||||
var_name = value.replace("os.environ/", "")
|
||||
return env_vars.get(var_name, os.getenv(var_name, value))
|
||||
|
||||
pattern = re.compile(r"\$\{?([A-Z0-9_]+)\}?")
|
||||
|
||||
def replace_match(match):
|
||||
var_name = match.group(1)
|
||||
return env_vars.get(var_name, os.getenv(var_name, match.group(0)))
|
||||
|
||||
return pattern.sub(replace_match, value)
|
||||
|
||||
|
||||
def clean_model_name(name):
|
||||
if not name:
|
||||
return ""
|
||||
return (
|
||||
name.replace("openai/", "")
|
||||
.replace("azure/", "")
|
||||
.replace("anthropic/", "")
|
||||
.replace("volcengine/", "")
|
||||
)
|
||||
|
||||
|
||||
def generate_api_base_hash(api_base):
|
||||
"""
|
||||
[同步更新] 生成 api_base 的 Hash,强制忽略 URL 的路径部分。
|
||||
保持与 volc_limit_sync.py 逻辑一致。
|
||||
"""
|
||||
if not api_base:
|
||||
return "default"
|
||||
|
||||
try:
|
||||
s = api_base.strip()
|
||||
parsed = urlparse(s)
|
||||
if parsed.scheme and parsed.netloc:
|
||||
clean_base = f"{parsed.scheme}://{parsed.netloc}".lower()
|
||||
else:
|
||||
clean_base = s.rstrip("/").lower()
|
||||
except Exception:
|
||||
clean_base = api_base.strip().rstrip("/").lower()
|
||||
|
||||
return hashlib.md5(clean_base.encode()).hexdigest()[:8]
|
||||
|
||||
|
||||
def strip_model_version_suffix(model_name):
|
||||
"""Convert foo-bar-260215 -> foo-bar for common versioned upstream names."""
|
||||
if not model_name:
|
||||
return ""
|
||||
|
||||
parts = model_name.rsplit("-", 1)
|
||||
if len(parts) == 2 and len(parts[1]) == 6 and parts[1].isdigit():
|
||||
return parts[0]
|
||||
|
||||
return model_name
|
||||
|
||||
|
||||
def load_python_constant(file_path, constant_name, default=None):
|
||||
"""Safely read a literal constant from volc_limit_sync.py without importing it."""
|
||||
if not os.path.exists(file_path):
|
||||
return default
|
||||
|
||||
try:
|
||||
with open(file_path, "r", encoding="utf-8") as f:
|
||||
source = f.read()
|
||||
module = ast.parse(source, filename=file_path)
|
||||
except Exception as e:
|
||||
print(f"⚠️ Warning: failed to parse {file_path}: {e}")
|
||||
return default
|
||||
|
||||
for node in module.body:
|
||||
if not isinstance(node, ast.Assign):
|
||||
continue
|
||||
for target in node.targets:
|
||||
if isinstance(target, ast.Name) and target.id == constant_name:
|
||||
try:
|
||||
return ast.literal_eval(node.value)
|
||||
except Exception as e:
|
||||
print(
|
||||
f"⚠️ Warning: failed to read constant {constant_name} from {file_path}: {e}"
|
||||
)
|
||||
return default
|
||||
|
||||
return default
|
||||
|
||||
|
||||
def build_monitored_model_aliases(monitored_models, manual_aliases):
|
||||
"""Mirror the monitored-model alias logic used by volc_limit_sync.py."""
|
||||
alias_map = {}
|
||||
|
||||
def add_alias(alias_name, target_model):
|
||||
clean_alias = clean_model_name(alias_name)
|
||||
clean_target = clean_model_name(target_model)
|
||||
if not clean_alias or not clean_target:
|
||||
return
|
||||
alias_map.setdefault(clean_alias, [])
|
||||
if clean_target not in alias_map[clean_alias]:
|
||||
alias_map[clean_alias].append(clean_target)
|
||||
|
||||
for target_model in sorted(set(monitored_models)):
|
||||
add_alias(target_model, target_model)
|
||||
|
||||
base_alias = strip_model_version_suffix(target_model)
|
||||
if base_alias != target_model:
|
||||
add_alias(base_alias, target_model)
|
||||
|
||||
for alias_name, targets in (manual_aliases or {}).items():
|
||||
if isinstance(targets, list):
|
||||
for target_model in targets:
|
||||
add_alias(alias_name, target_model)
|
||||
else:
|
||||
add_alias(alias_name, targets)
|
||||
|
||||
return alias_map
|
||||
|
||||
|
||||
def is_monitored_volc_entry(api_base_raw, api_base_resolved):
|
||||
if not isinstance(api_base_raw, str):
|
||||
return False
|
||||
resolved_text = str(api_base_resolved or "").lower()
|
||||
return "HUOSHAN_BASE_URL" in api_base_raw or "volcengineapi" in resolved_text
|
||||
|
||||
|
||||
def collect_monitored_entries(config, env_vars):
|
||||
entries = {}
|
||||
model_list = config.get("model_list", [])
|
||||
|
||||
for model in model_list:
|
||||
litellm_params = model.get("litellm_params", {})
|
||||
api_base_raw = litellm_params.get("api_base", "")
|
||||
api_base_resolved = resolve_env_var(api_base_raw, env_vars)
|
||||
|
||||
if not is_monitored_volc_entry(api_base_raw, api_base_resolved):
|
||||
continue
|
||||
|
||||
model_alias = model.get("model_name", "unknown")
|
||||
upstream_model = clean_model_name(litellm_params.get("model", ""))
|
||||
if not upstream_model:
|
||||
continue
|
||||
|
||||
base_hash = generate_api_base_hash(api_base_resolved)
|
||||
final_key = f"{upstream_model}:{base_hash}"
|
||||
|
||||
entry = entries.setdefault(
|
||||
final_key,
|
||||
{
|
||||
"config_aliases": [],
|
||||
"upstream_model": upstream_model,
|
||||
"api_base_raw": api_base_raw,
|
||||
"api_base_resolved": api_base_resolved,
|
||||
"base_hash": base_hash,
|
||||
"final_key": final_key,
|
||||
},
|
||||
)
|
||||
|
||||
if model_alias not in entry["config_aliases"]:
|
||||
entry["config_aliases"].append(model_alias)
|
||||
|
||||
return list(entries.values())
|
||||
|
||||
|
||||
def get_limit_rule(clean_model, base_hash, hard_limits):
|
||||
specific_key = f"{clean_model}:{base_hash}"
|
||||
if specific_key in hard_limits:
|
||||
return specific_key, hard_limits[specific_key]
|
||||
|
||||
if clean_model in hard_limits:
|
||||
return clean_model, hard_limits[clean_model]
|
||||
|
||||
for quota_model, limit_val in hard_limits.items():
|
||||
if quota_model == "__DEFAULT__":
|
||||
continue
|
||||
if ":" not in quota_model and quota_model in clean_model:
|
||||
return quota_model, limit_val
|
||||
|
||||
if "__DEFAULT__" in hard_limits:
|
||||
return "__DEFAULT__", hard_limits["__DEFAULT__"]
|
||||
|
||||
return None, None
|
||||
|
||||
|
||||
def build_reverse_alias_map(alias_map):
|
||||
reverse_map = {}
|
||||
for alias_name, targets in alias_map.items():
|
||||
for target_model in targets:
|
||||
reverse_map.setdefault(target_model, [])
|
||||
if alias_name not in reverse_map[target_model]:
|
||||
reverse_map[target_model].append(alias_name)
|
||||
return reverse_map
|
||||
|
||||
|
||||
def format_limit_source(rule_name, limit_value, final_key, upstream_model):
|
||||
if rule_name is None:
|
||||
return "未配置"
|
||||
if rule_name == final_key:
|
||||
return f"自定义键 {rule_name} => {int(limit_value):,}"
|
||||
if rule_name == upstream_model:
|
||||
return f"模型级规则 {rule_name} => {int(limit_value):,}"
|
||||
if rule_name == "__DEFAULT__":
|
||||
return f"默认规则 __DEFAULT__ => {int(limit_value):,}"
|
||||
return f"模糊规则 {rule_name} => {int(limit_value):,}"
|
||||
|
||||
|
||||
def print_monitored_audit(entries, reverse_alias_map, hard_limits):
|
||||
print("=== 监控模型审计 / config.yaml -> volc_limit_sync.py ===")
|
||||
print()
|
||||
|
||||
for entry in entries:
|
||||
monitor_aliases = reverse_alias_map.get(
|
||||
entry["upstream_model"], [entry["upstream_model"]]
|
||||
)
|
||||
rule_name, limit_value = get_limit_rule(
|
||||
entry["upstream_model"], entry["base_hash"], hard_limits
|
||||
)
|
||||
|
||||
print(f"- Config alias(es): {', '.join(entry['config_aliases'])}")
|
||||
print(f" Upstream model : {entry['upstream_model']}")
|
||||
print(f" 监控别名 : {', '.join(monitor_aliases)}")
|
||||
print(f" Generated key : {entry['final_key']}")
|
||||
print(
|
||||
f" 限额来源 : {format_limit_source(rule_name, limit_value, entry['final_key'], entry['upstream_model'])}"
|
||||
)
|
||||
print()
|
||||
|
||||
|
||||
def print_maintenance_guide(entries, manual_aliases, hard_limits):
|
||||
default_limit = hard_limits.get("__DEFAULT__")
|
||||
|
||||
print("=== 维护入口 ===")
|
||||
print("1. 在 config.yaml 的 model_list 中增删火山模型,这是“纳入监控模型”的主入口。")
|
||||
print("2. 改完后运行 python generate_keys.py,先看上面的“监控别名”和“限额来源”。")
|
||||
print("3. 常规版本号模型(如 xxx-260215)会自动推导成监控别名 xxx,通常不用再改 volc_limit_sync.py。")
|
||||
print("4. 只有两种情况才需要改 volc_limit_sync.py 的 MANUAL_MONITORED_MODEL_ALIASES:")
|
||||
print(" - 资源包名和上游模型名不一致。")
|
||||
print(" - 一个资源包要映射到多个上游模型。")
|
||||
if manual_aliases:
|
||||
print(
|
||||
f"5. 当前手工别名覆盖: {', '.join(sorted(manual_aliases.keys()))}"
|
||||
)
|
||||
print()
|
||||
|
||||
defaulted_entries = []
|
||||
for entry in entries:
|
||||
rule_name, _ = get_limit_rule(
|
||||
entry["upstream_model"], entry["base_hash"], hard_limits
|
||||
)
|
||||
if rule_name == "__DEFAULT__":
|
||||
defaulted_entries.append(entry)
|
||||
|
||||
if defaulted_entries and default_limit is not None:
|
||||
print("📋 可选:以下监控模型当前走 __DEFAULT__,如需单模型硬限额可复制到 HARD_DAILY_LIMITS")
|
||||
print("HARD_DAILY_LIMITS = {")
|
||||
for entry in defaulted_entries:
|
||||
print(f' "{entry["final_key"]}": {int(default_limit)},')
|
||||
print("}")
|
||||
print()
|
||||
|
||||
|
||||
def main():
|
||||
print("🔍 Auditing monitored Volcengine models from config.yaml...\n")
|
||||
|
||||
env_vars = load_env(ENV_FILE)
|
||||
|
||||
if not os.path.exists(CONFIG_FILE):
|
||||
print(f"❌ Error: {CONFIG_FILE} not found!")
|
||||
return
|
||||
|
||||
try:
|
||||
with open(CONFIG_FILE, "r", encoding="utf-8") as f:
|
||||
config = yaml.safe_load(f)
|
||||
except Exception as e:
|
||||
print(f"❌ Error parsing yaml: {e}")
|
||||
return
|
||||
|
||||
entries = collect_monitored_entries(config or {}, env_vars)
|
||||
if not entries:
|
||||
print("⚠️ No monitored Volcengine models found in config.yaml")
|
||||
return
|
||||
|
||||
entries.sort(key=lambda item: item["upstream_model"])
|
||||
|
||||
manual_aliases = load_python_constant(
|
||||
RUNTIME_FILE, "MANUAL_MONITORED_MODEL_ALIASES", default={}
|
||||
) or {}
|
||||
hard_limits = load_python_constant(RUNTIME_FILE, "HARD_DAILY_LIMITS", default={}) or {}
|
||||
|
||||
monitored_upstreams = [entry["upstream_model"] for entry in entries]
|
||||
alias_map = build_monitored_model_aliases(monitored_upstreams, manual_aliases)
|
||||
reverse_alias_map = build_reverse_alias_map(alias_map)
|
||||
|
||||
print_monitored_audit(entries, reverse_alias_map, hard_limits)
|
||||
print_maintenance_guide(entries, manual_aliases, hard_limits)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user