textmachine/eval/exp14/exp14_common.py

191 lines
9.8 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
"""exp14 — общий харнес: провайдер-вызов, usage→$ (3 reasoning-режима), per-call
predicted-cost кап (НЕ group-level — урок exp13 +10%), append-only персист usage/cost,
токенайзер-счёт выходных токенов (кириллица-фертильность), spec-фабрика.
Зона eval/. Цены — ЗАМОРОЖЕННАЯ зеркальная копия backend/configs/models.yaml
(prices_checked 2026-07-10) + фронтир gpt-5.4 live-фактчек 2026-07-11
(developers.openai.com/api/docs/pricing). Единственный источник истины — models.yaml;
при расхождении верить Go/models.yaml. Ключи из eval/.env через load_env_file (НЕ читаю .env).
"""
from __future__ import annotations
import json, os, sys, time, urllib.request, urllib.error
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) # script now under eval/expNN/ — reach up to parent eval/ for the refusal_bench oracle
from refusal_bench import load_env_file # noqa: E402
load_env_file()
DIAG = Path("/home/ubuntu/books/gu-zhenren/exp14")
DIAG.mkdir(parents=True, exist_ok=True)
RERUN = Path("/home/ubuntu/books/gu-zhenren/rerun")
TOKDIR = Path(__file__).resolve().parent / "tokenizers"
# ── Цены $/1M (in, out, cached_in) + reasoning-режим. Зеркало models.yaml. ──────────
PRICES = {
"deepseek-v4-flash": (0.14, 0.28, 0.0028, "subset"),
"deepseek-v4-pro": (0.435, 0.87, 0.003625, "subset"),
"glm-5": (1.0, 3.2, 0.2, "subset"),
"glm-5.1": (1.4, 4.4, 0.26, "subset"),
"kimi-k2.6": (0.95, 4.0, 0.16, "subset"),
"grok-4.3": (1.25, 2.50, 1.25, "additive"),
"gemini-3.1-pro-preview": (2.0, 12.0, 0.20, "additive_total"),
"gpt-5-mini": (0.25, 2.0, 0.025, "subset"),
"gpt-5.4": (2.50, 15.0, 0.25, "subset"), # live 2026-07-11
"mistral-large-2512": (0.5, 1.5, 0.5, "subset"),
}
FALLBACK = "deepseek-v4-flash" # никогда $0
# ── Спеки провайдеров (endpoint/keyenv/wire-квирки из 00-provider-quirks + models.yaml) ─
def spec(model: str, *, temp: float | None = None, max_tokens: int | None = None,
reasoning_effort: str | None = None) -> dict:
"""Собирает per-call spec. temp/max_tokens/reasoning_effort перекрывают дефолты."""
base_by = {
"deepseek-v4-flash": ("https://api.deepseek.com/v1", "DEEPSEEK_API_KEY", {}),
"deepseek-v4-pro": ("https://api.deepseek.com/v1", "DEEPSEEK_API_KEY", {}),
"glm-5": ("https://api.z.ai/api/paas/v4", "ZAI_API_KEY", {"thinking": {"type": "disabled"}}),
"glm-5.1": ("https://api.z.ai/api/paas/v4", "ZAI_API_KEY", {"thinking": {"type": "disabled"}}),
"kimi-k2.6": ("https://api.moonshot.ai/v1", "KIMI_API_KEY", {}),
"grok-4.3": ("https://api.x.ai/v1", "XAI_API_KEY", {}),
"gemini-3.1-pro-preview": ("https://generativelanguage.googleapis.com/v1beta/openai", "GEMINI_API_KEY", {}),
"gpt-5-mini": ("https://api.openai.com/v1", "OPENAI_API_KEY", {}),
"gpt-5.4": ("https://api.openai.com/v1", "OPENAI_API_KEY", {}),
"mistral-large-2512": ("https://api.mistral.ai/v1", "MISTRAL_API_KEY", {"safe_prompt": False}),
}
base, keyenv, extra = base_by[model]
extra = dict(extra)
s = {"model": model, "base": base, "keyenv": keyenv, "extra": extra}
# reasoning wire per family
if model in ("gpt-5-mini", "gpt-5.4"):
s["openai_reasoner"] = True # max_completion_tokens, no temperature
extra["reasoning_effort"] = reasoning_effort or "medium"
elif model == "kimi-k2.6":
s["force_temp"] = 1.0
elif model == "grok-4.3":
if reasoning_effort: # xAI plain reasoning_effort in body (none/low/medium/high)
extra["reasoning_effort"] = reasoning_effort
# temp
if model in ("gpt-5-mini", "gpt-5.4"):
s["temp"] = None
elif model == "kimi-k2.6":
s["temp"] = 1.0
else:
s["temp"] = 0.4 if temp is None else temp
# max_tokens floors (D24.3)
floor = 16000 if model == "kimi-k2.6" else (12000 if model == "gpt-5.4" else 8000)
s["max_tokens"] = max_tokens or floor
return s
def call(s: dict, messages: list[dict], timeout: int = 300) -> tuple[str, str | None, dict]:
"""Один chat/completions. Возврат (text, err|None, usage со стампом finish_reason).
err — структурная строка: http|CODE|.. / transport|.. / content_filter|.. / empty|.."""
key = os.environ.get(s["keyenv"], "")
body = {"model": s["model"], "messages": messages, **s.get("extra", {})}
if s.get("openai_reasoner"):
body["max_completion_tokens"] = s["max_tokens"] # gpt-5: max_tokens → 400
else:
body["max_tokens"] = s["max_tokens"]
if s.get("temp") is not None:
body["temperature"] = s["temp"]
data = json.dumps(body).encode()
req = urllib.request.Request(s["base"].rstrip("/") + "/chat/completions", data=data,
headers={"Content-Type": "application/json", "Authorization": f"Bearer {key}"})
try:
with urllib.request.urlopen(req, timeout=timeout) as r:
d = json.loads(r.read())
except urllib.error.HTTPError as e:
return "", f"http|{e.code}|{e.read()[:300].decode('utf-8','replace')}", {}
except Exception as e:
return "", f"transport|{type(e).__name__}|{str(e)[:200]}", {}
ch = (d.get("choices") or [{}])[0]
finish = ch.get("finish_reason", "")
msg = ch.get("message", {}) or {}
text = (msg.get("content") or "").strip()
usage = d.get("usage", {}) or {}
usage["finish_reason"] = finish
if str(finish).lower() in ("content_filter", "prohibited_content", "safety") or "prohibited" in str(finish).lower():
return "", f"content_filter|finish={finish}", usage
if not text:
has_reason = bool((msg.get("reasoning_content") or "").strip())
return "", f"empty|finish={finish}|reasoning={has_reason}", usage
return text, None, usage
def cost_of(model: str, usage: dict) -> float:
"""usage→$ с 3 reasoning-режимами (subset/additive/additive_total)."""
inp = usage.get("prompt_tokens", 0) or 0
comp = usage.get("completion_tokens", 0) or 0
total = usage.get("total_tokens", 0) or 0
pin, pout, _c, regime = PRICES.get(model, (*PRICES[FALLBACK][:3], "subset"))
if regime == "additive": # grok: reasoning_tokens отдельным полем, поверх completion
reason = usage.get("reasoning_tokens", 0) or 0
out = comp + reason
elif regime == "additive_total": # gemini: thinking только в total
out = max(comp, total - inp) if total else comp
else: # subset: reasoning ⊆ completion
out = comp
return (inp * pin + out * pout) / 1_000_000
def predict_cost(model: str, in_tokens_est: int, max_out: int) -> float:
"""Верхняя оценка ДО вызова: in_est·pin + max_out·pout (потолок max_tokens как output)."""
pin, pout, _c, _r = PRICES.get(model, (*PRICES[FALLBACK][:3], "subset"))
return (in_tokens_est * pin + max_out * pout) / 1_000_000
# ── Токенайзеры (кириллица-фертильность; deepseek-v4/kimi отсутствуют → v3.2 прокси) ──
_TOK = {}
def _load_tok(name: str):
from tokenizers import Tokenizer
p = TOKDIR / name / "tokenizer.json"
if name not in _TOK:
_TOK[name] = Tokenizer.from_file(str(p))
return _TOK[name]
def count_tokens(text: str, family: str = "glm") -> int:
name = {"glm": "glm-4.6", "deepseek": "deepseek-v3.2"}.get(family, "glm-4.6")
return len(_load_tok(name).encode(text, add_special_tokens=False).ids)
# ── Per-call кап + персист ────────────────────────────────────────────────────────
class Spender:
"""Per-key running spend + per-call predicted-cost кап (обязателен ДО вызова)."""
def __init__(self, costs_path: Path, caps: dict[str, float]):
self.path = costs_path
self.caps = caps # model → per-call cap $
self.by_key: dict[str, float] = {}
def guard(self, model: str, in_est: int, max_out: int) -> tuple[bool, float]:
pc = predict_cost(model, in_est, max_out)
cap = self.caps.get(model, 0.50)
return (pc <= cap, pc)
def record(self, rec: dict) -> float:
c = cost_of(rec["model"], rec.get("usage", {}))
rec["cost"] = round(c, 6)
keyenv = rec.get("keyenv", rec["model"])
self.by_key[keyenv] = self.by_key.get(keyenv, 0.0) + c
with open(self.path, "a", encoding="utf-8") as f:
f.write(json.dumps(rec, ensure_ascii=False) + "\n")
return c
def messages_with_injection(system_prefix: str, injection: str, user: str) -> list[dict]:
"""Layout render.go MessagesWithInjection: system(prefix,cache) → system(inj) → user."""
m = [{"role": "system", "content": system_prefix}]
if injection.strip():
m.append({"role": "system", "content": injection})
m.append({"role": "user", "content": user})
return m
if __name__ == "__main__":
# смоук: цены/токенайзер/предикт
ru = "Он шёл по тёмной улице, и снег ложился ему на плечи, укрывая следы."
print("glm-4.6 tokens:", count_tokens(ru, "glm"))
print("deepseek(v3.2) tokens:", count_tokens(ru, "deepseek"))
u = {"prompt_tokens": 5000, "completion_tokens": 3000, "total_tokens": 8200}
for m in ("glm-5", "gpt-5.4", "gemini-3.1-pro-preview", "grok-4.3"):
print(f"cost_of {m}: ${cost_of(m, u):.5f} predict(5k in, 8k out): ${predict_cost(m,5000,8000):.4f}")