textmachine/eval/bank_arbitration/frontier_probe.py

235 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""Проба C (промт POLYGON_EDITOR_WIRE_PROBE §C): фронтир-пасс по голду — «сколько докупает ум».
Тот же терминолог-протокол, что research/24 §B3 (`consilium_probe.py`: ростер 63 строки, 5 батчей,
engine-faithful RenderBatch, боевой terminologist.md, добавка CONF_NUM), против чемпиона glm-5 25/45.
Кандидаты (пре-рег §0C, слаги live /models 04.08): deepseek-v4-pro · gpt-5.6-terra · gpt-5.6-luna.
Цены — прайс-страница вендора, короткий контекст-тир (батчи ≈4.5k ток.).
⚠ Wire-квирк GPT-5.6 НЕ гадаем: `reasoning.mode`/`reasoning.effort` задокументированы для Responses
API; плоский `reasoning_effort` в Chat Completions вендором не заявлен, а прецедент exp08 (xAI) —
вложенная форма МОЛЧА ГЛОТАЕТСЯ. Поэтому `--wirecheck` меряет ручку живьём (none против high по
reasoning_tokens) ДО основного прогона, и транспорт выбирается по замеру.
"""
from __future__ import annotations
import argparse
import json
import os
import sys
import time
from pathlib import Path
from dotenv import load_dotenv
from openai import OpenAI
REPO = Path("/home/ubuntu/projects/textmachine")
HERE = Path(__file__).resolve().parent
RAW = Path.home() / "books" / "gu-zhenren" / "bank-arbitration"
load_dotenv(REPO / "eval" / ".env")
sys.path.insert(0, str(HERE))
from consilium_probe import (build_roster, batches_of, block_of, render_terminologist, # noqa: E402
CONF_NUM)
from arbitrate import GOLD, parse_pass, eq # noqa: E402
CEILING_USD = 0.50 # потолок пробы C (промт §C)
HARD_STOP_USD = 0.45 # пре-рег: живой стоп ниже потолка
# Пер-модельные потолки вывода. Гард ПРОЕКЦИОННЫЙ: вызов не делается, если ХУДШИЙ его исход
# (упор в cap) пробил бы CEILING_USD — иначе стоп «по факту» ловил бы уже потраченное.
CAPS = {"deepseek-v4-pro": 16000, "gpt-5.6-luna": 16000, "gpt-5.6-terra": 8000}
ORDER = ["deepseek-v4-pro", "gpt-5.6-luna", "gpt-5.6-terra"] # дешёвые первыми
# (base_url, env, in, cached_in, out, openai_family)
CAND = {
"deepseek-v4-pro": ("https://api.deepseek.com/v1", "DEEPSEEK_API_KEY", 0.435, 0.003625, 0.87, False),
"gpt-5.6-terra": ("https://api.openai.com/v1", "OPENAI_API_KEY", 2.00, 0.20, 12.00, True),
"gpt-5.6-luna": ("https://api.openai.com/v1", "OPENAI_API_KEY", 0.20, 0.02, 1.20, True),
}
TAG = {"deepseek-v4-pro": "fdp", "gpt-5.6-terra": "fte", "gpt-5.6-luna": "flu"}
WIRE = HERE / "gold" / "frontier_wire.json" # результат --wirecheck: как слать reasoning
def client(model: str) -> OpenAI:
base, env, *_ = CAND[model]
return OpenAI(api_key=os.environ[env], base_url=base, timeout=900)
def price(model: str, pt: int, cached: int, ct: int) -> float:
_, _, pin, pcache, pout, _ = CAND[model]
return (pt - cached) / 1e6 * pin + cached / 1e6 * pcache + ct / 1e6 * pout
def usage_of(u):
pt = getattr(u, "prompt_tokens", 0) or 0
ct = getattr(u, "completion_tokens", 0) or 0
cached = getattr(getattr(u, "prompt_tokens_details", None), "cached_tokens", 0) or 0
rt = getattr(getattr(u, "completion_tokens_details", None), "reasoning_tokens", 0) or 0
return pt, cached, ct, rt
def call(model: str, system: str, user: str, tag: str, effort: str | None, cap: int) -> dict:
_, _, _, _, _, is_openai = CAND[model]
cl = client(model)
msgs = [{"role": "system", "content": system}, {"role": "user", "content": user}]
kw: dict = dict(model=model, messages=msgs)
if is_openai:
kw["max_completion_tokens"] = cap # quirks 00: не max_tokens
if effort: # temperature не шлём (reasoning-модель)
kw["reasoning_effort"] = effort
else:
kw["max_tokens"] = cap
kw["temperature"] = 0
# v4-pro: reasoning-параметр НЕ шлём (low маппится в high; ручки бюджета нет)
t0 = time.time()
r = cl.chat.completions.create(**kw)
ch = r.choices[0]
pt, cached, ct, rt = usage_of(r.usage)
cost = price(model, pt, cached, ct)
rec = dict(tag=tag, model=model, model_returned=r.model, effort=effort or "default",
finish=ch.finish_reason, ts=time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
cap=cap, prompt_tokens=pt, cached_tokens=cached, completion_tokens=ct,
reasoning_tokens=rt, cost_usd=round(cost, 6), latency_s=round(time.time() - t0, 1),
content=ch.message.content or "", system=system, user=user)
(RAW / f"{tag}.json").write_text(json.dumps(rec, ensure_ascii=False, indent=1), encoding="utf-8")
print(f"[{tag}] {model} eff={effort} finish={ch.finish_reason} in={pt}(c{cached}) out={ct}"
f"(r{rt}) ${cost:.6f} {rec['latency_s']}s content={len(rec['content'])}c")
return rec
def cmd_slugcheck():
for model in CAND:
base, env, *_ = CAND[model]
ids = [m.id for m in OpenAI(api_key=os.environ[env], base_url=base, timeout=120).models.list().data]
print(f"/models {model}: {'OK' if model in ids else 'ОТСУТСТВУЕТ'} (всего {len(ids)})")
if model not in ids:
raise SystemExit(f"слаг {model} не листится — СТОП+релей")
def cmd_wirecheck():
"""Живой замер ручки reasoning на OpenAI-кандидатах: none против high по reasoning_tokens.
Ручка ЖИВАЯ ⇔ reasoning_tokens различаются. Плюс проверка, что Chat Completions вообще принимает."""
probe = "Ответь ровно одним словом: сколько будет 17*23?"
out = {}
total = 0.0
for model in CAND:
if not CAND[model][5]:
out[model] = {"transport": "chat", "effort": None, "note": "v4-pro: параметр не шлём (дефолт high)"}
continue
res = {}
for eff in ("none", "high"):
try:
r = call(model, "Ты — калькулятор.", probe, f"wire-{TAG[model]}-{eff}", eff, 2000)
res[eff] = r["reasoning_tokens"]
total += r["cost_usd"]
except Exception as e:
res[eff] = f"ERR {type(e).__name__}: {str(e)[:200]}"
live = isinstance(res.get("none"), int) and isinstance(res.get("high"), int) and res["none"] != res["high"]
out[model] = {"transport": "chat" if live else "НЕ ПОДТВЕРЖДЕНО",
"reasoning_tokens": res, "knob_live": live,
"effort": "low" if live else None}
print(f"{model}: ручка {'ЖИВАЯ' if live else 'НЕ подтверждена'} {res}")
WIRE.write_text(json.dumps(out, ensure_ascii=False, indent=1), encoding="utf-8")
print(f"wirecheck стоил ${total:.6f}; вердикты → {WIRE}")
def cmd_plan():
roster = build_roster()
batches = batches_of(roster)
print(f"ростер={len(roster)} строк, батчей={len(batches)}")
# вход берём фактический из сырья §B3 (тот же промпт и те же батчи)
est_in = sum(json.load(open(RAW / f"glm1-b{i}.json", encoding="utf-8"))["prompt_tokens"]
for i in range(len(batches)))
for model in CAND:
for out_est in (5000, 15000, 40000):
print(f" {model:18s} вход {est_in} ток. + выход {out_est:6d} → ${price(model, est_in, 0, out_est):.4f}")
print(f"ПОТОЛОК ПРОБЫ $0.50; живой стоп харнесса ${HARD_STOP_USD}")
def cmd_run(models: list[str]):
wire = json.load(open(WIRE, encoding="utf-8")) if WIRE.exists() else {}
roster = build_roster()
batches = batches_of(roster)
total = sum(json.load(open(f, encoding="utf-8"))["cost_usd"]
for m in CAND for f in RAW.glob(f"{TAG[m]}-b*.json"))
if total:
print(f"уже потрачено пробой C по сырью: ${total:.6f}")
for model in [m for m in ORDER if m in models]:
eff = wire.get(model, {}).get("effort")
cap = CAPS[model]
for i, b in enumerate(batches):
tag = f"{TAG[model]}-b{i}"
if (RAW / f"{tag}.json").exists():
print(f"[{tag}] уже персистирован — пропуск")
continue
system, user = render_terminologist("\n\n".join(block_of(c) for c in b))
worst = price(model, len(user) // 2 + 2000, 0, cap) # худший исход ЭТОГО вызова
if total + worst > CEILING_USD or total >= HARD_STOP_USD:
print(f"СТОП (проекционный гард): накоплено ${total:.4f}, худший исход вызова "
f"${worst:.4f} → пробил бы потолок ${CEILING_USD}. Остаток арма не гоню.")
return
total += call(model, system, user + CONF_NUM, tag, eff, cap)["cost_usd"]
time.sleep(1.0)
print(f"TOTAL пробы C: ${total:.6f}")
def cmd_score():
gb = {r["bank_src"]: r for r in GOLD if r["in_bank"]}
passes = {"glm1 (чемпион)": "glm1", "ds1 (flash low)": "ds1", "mis1": "mis1"}
passes.update({m: TAG[m] for m in CAND})
acc, cost, rows = {}, {}, {}
for name, tag in passes.items():
p = parse_pass(tag)
if not p["map"]:
continue
acc[name] = {s for s, g in gb.items() if eq(p["map"].get(s, ("", None))[0], g["gold_dst"])}
cost[name] = p["cost"]
rows[name] = len(p["map"])
print("=== Точность против голда (45) и деньги пасса ===")
for n in acc:
print(f" {n:22s} {len(acc[n])}/45 строк={rows[n]} ${cost[n]:.6f} "
f"пер-терм ${cost[n]/63:.6f}")
print("\n=== Парное против чемпиона glm-5 (критерий пре-рега: ≥ +5) ===")
base = acc.get("glm1 (чемпион)")
for n in acc:
if n == "glm1 (чемпион)":
continue
x, y = len(acc[n] - base), len(base - acc[n])
verdict = "ОПРАВДАН" if x - y >= 5 else "не оправдан"
print(f" {n:22s} +{x} / -{y} (net {x-y:+d}) → {verdict}")
print("\n=== Цена/книга (методика research/24 §B5: 2000 различных строк банка) ===")
for n in acc:
print(f" {n:22s} ${cost[n]/63*2000:.2f}/книга")
print("\n=== Единицы: что кандидат берёт, а glm-5 теряет (и наоборот) ===")
for n in acc:
if n == "glm1 (чемпион)":
continue
for s in sorted(acc[n] - base):
print(f" +{n} {s}: gold={gb[s]['gold_dst']!r}")
for s in sorted(base - acc[n]):
print(f" -{n} {s}: gold={gb[s]['gold_dst']!r}")
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--slugcheck", action="store_true")
ap.add_argument("--wirecheck", action="store_true")
ap.add_argument("--plan", action="store_true")
ap.add_argument("--run", nargs="*", default=None)
ap.add_argument("--score", action="store_true")
a = ap.parse_args()
if a.slugcheck:
cmd_slugcheck()
elif a.wirecheck:
cmd_wirecheck()
elif a.plan:
cmd_plan()
elif a.run is not None:
cmd_run(a.run or list(CAND))
elif a.score:
cmd_score()
else:
ap.print_help()
if __name__ == "__main__":
main()