Files
sub2api/_parse_upstream.py

79 lines
3.4 KiB
Python
Raw Normal View History

feat(channel-monitor): request templates with snapshot apply + headers/body override Problem: Upstream channels can reject monitor probes based on client fingerprint (e.g. "only Claude Code clients allowed"). The monitor had no way to customize the outgoing request to bypass such restrictions. Solution: Introduce reusable request templates that carry extra_headers plus an optional body override; monitors reference a template and receive a snapshot copy on apply. Template edits do NOT auto-propagate — users must click "apply to associated monitors" to refresh snapshots, so a bad template edit cannot instantly break all production monitors. Data model (migration 112): - channel_monitor_request_templates: id, name, provider, description, extra_headers jsonb, body_override_mode ('off'|'merge'|'replace'), body_override jsonb. Unique (provider, name). - channel_monitors: +template_id (FK, ON DELETE SET NULL), +extra_headers, +body_override_mode, +body_override (the three runtime snapshot fields). Checker (channel_monitor_checker.go): - callProvider + runCheckForModel accept a CheckOptions carrying the snapshot fields. mergeHeaders applies user headers on top of adapter defaults (forbidden list: Host / Content-Length / Transfer-Encoding / Connection / Content-Encoding). - buildRequestBody: off -> adapter default body merge -> shallow-merge over default; per-provider deny list (model/messages/contents) protects the challenge contract replace -> user body verbatim - Replace mode skips challenge validation; instead HTTP 2xx + non-empty extracted response text = operational, empty = failed. - 4 new unit tests cover all three modes + replace/empty-response case. Admin API: - /admin/channel-monitor-templates CRUD + /:id/apply (overwrite snapshot on all template_id=id monitors, returns affected count). - channel_monitor request/response DTOs gain the 4 new fields. Frontend: - channelMonitorTemplate.ts API client. - MonitorAdvancedRequestConfig.vue shared component for headers textarea + body mode radio + body JSON editor; used by both template and monitor forms. - MonitorTemplateManagerDialog.vue: provider tabs, list/create/edit/ delete/apply, live "associated monitors" count per row. - MonitorFiltersBar: new 模板管理 button next to 新增监控. - MonitorFormDialog: collapsible 高级 section with template dropdown (filtered by form.provider, clears on provider change) + embedded AdvancedRequestConfig. Picking a template copies its fields into the form (snapshot semantics mirrored on the client). - i18n zh/en entries for all new copy. chore: bump version to 0.1.114.32
2026-04-21 14:14:49 +08:00
"""
严格按模型拆分 upstream token quota并按我们的定价表重算每个模型的 token 应得金额
对比 upstream provider-side (/group_ratio) 与我们 Anthropic 官方价的计算结果
"""
import re
import json
from collections import defaultdict
# 按账号(token_name) + 模型拆
by_key = defaultdict(lambda: {
'count': 0,
'prompt': 0,
'completion': 0,
'cache_create': 0,
'cache_read': 0,
'quota_pre_group_sum': 0.0,
'flat_price_reqs': 0,
'flat_price_value': 0.0,
'model_ratios': set(),
'model_prices': set(),
})
with open(r"C:\Users\16790\xwechat_files\wxid_8tc8tfooo5rs22_fef8\msg\file\2026-04\asakifeng_consume.txt", 'r', encoding='utf-8') as f:
for line in f:
m = re.match(r'\[INFO\] (\d{4}/\d{2}/\d{2} - \d{2}:\d{2}:\d{2}) \|.*params=(\{.*\})\s*$', line.strip())
if not m: continue
try: p = json.loads(m.group(2))
except Exception: continue
tn = p.get('token_name', '')
model = p.get('model_name', '')
other = p.get('other') or {}
gr = other.get('group_ratio', 1.0) or 1.0
q = p.get('quota', 0) or 0
k = (tn, model)
d = by_key[k]
d['count'] += 1
d['prompt'] += p.get('prompt_tokens', 0) or 0
d['completion'] += p.get('completion_tokens', 0) or 0
d['cache_create'] += other.get('cache_creation_tokens', 0) or 0
d['cache_read'] += other.get('cache_tokens', 0) or 0
d['quota_pre_group_sum'] += q / gr if gr else q
mp = other.get('model_price') or 0
mr = other.get('model_ratio')
if mr is not None: d['model_ratios'].add(mr)
d['model_prices'].add(mp)
if mp and mp > 0:
d['flat_price_reqs'] += 1
d['flat_price_value'] += mp # flat $ per request
# 我们定价表(从 backend/resources/.../model_prices_and_context_window.json 读的真实值)
OUR_PRICE = {
'claude-haiku-4-5-20251001': {'input': 1e-6, 'output': 5e-6, 'cc5m': 1.25e-6, 'cr': 1e-7},
'claude-sonnet-4-6': {'input': 3e-6, 'output': 1.5e-5, 'cc5m': 3.75e-6, 'cr': 3e-7},
'claude-sonnet-4-5-20250929': {'input': 3e-6, 'output': 1.5e-5, 'cc5m': 3.75e-6, 'cr': 3e-7},
'claude-opus-4-6': {'input': 5e-6, 'output': 2.5e-5, 'cc5m': 6.25e-6, 'cr': 5e-7},
'claude-opus-4-5-20251101': {'input': 5e-6, 'output': 2.5e-5, 'cc5m': 6.25e-6, 'cr': 5e-7},
'claude-opus-4-7': {'input': 5e-6, 'output': 2.5e-5, 'cc5m': 6.25e-6, 'cr': 5e-7}, # 我们回退到 opus-4-6 价
}
print("%-40s %-28s %5s %12s %12s %12s %12s" % ("TOKEN", "MODEL", "req", "upstream$", "our_calc$", "diff$", "note"))
print("-" * 150)
total_up = 0.0; total_ours = 0.0
for (tn, model), d in sorted(by_key.items()):
up = d['quota_pre_group_sum'] / 500000
p = OUR_PRICE.get(model)
if p:
ours = (d['prompt']*p['input'] + d['completion']*p['output']
+ d['cache_create']*p['cc5m'] + d['cache_read']*p['cr'])
else:
ours = 0.0
diff = up - ours
note = ""
if d['flat_price_reqs']:
note = f"flat_price {d['flat_price_reqs']}/{d['count']}"
total_up += up; total_ours += ours
print("%-40s %-28s %5d %12.4f %12.4f %+12.4f %s" % (tn[:40], model, d['count'], up, ours, diff, note))
print("-" * 150)
print("%-40s %-28s %5s %12.4f %12.4f %+12.4f" % ("TOTAL", "", "", total_up, total_ours, total_up - total_ours))