5c8c13891f
Build and deploy / build-deploy (push) Has been cancelled
- миграция checks: nullable input_tokens/output_tokens/total_tokens/usage_json (идемпотентно, старые строки остаются NULL; probe мигрирует сам — cron стартует раньше uvicorn) - probe.py и /api/check фиксируют usage ДО валидации контента: токены сохраняются при пустом/сломанном ответе; таймауты и ответы без usage = NULL, не нули - usage_json хранит сырой usage провайдера (OpenAI/Anthropic/camelCase форматы) - /api/checks и сегментная история несут токены; новый /api/token_stats: rolling 24h + all time, покрытие known/unknown, разбивка по провайдерам (провайдер из models с fallback на префикс model_id) - UI: статы «Токены 24h» и «Покрытие usage» в шапке, токены в тултипах сегментов и логе проверок - статика через __file__-относительный путь (работает и в /app/app, и локально) - тесты (30, реальные HTTP против мока OmniRoute): миграция/идемпотентность, NULL при missing/zero usage, usage при упавшем контенте, таймаут, агрегаты, оба пути записи (probe.run + /api/check)
71 lines
2.5 KiB
Python
71 lines
2.5 KiB
Python
"""extract_usage: разбор форматов usage разных провайдеров."""
|
|
import sys
|
|
import os
|
|
|
|
sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "app"))
|
|
|
|
from usage_utils import extract_usage
|
|
|
|
EMPTY = {"input_tokens": None, "output_tokens": None, "total_tokens": None, "usage_json": None}
|
|
|
|
|
|
def test_openai_shape():
|
|
u = extract_usage({"choices": [], "usage": {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15}})
|
|
assert u["input_tokens"] == 10
|
|
assert u["output_tokens"] == 5
|
|
assert u["total_tokens"] == 15
|
|
assert u["usage_json"] == '{"completion_tokens": 5, "prompt_tokens": 10, "total_tokens": 15}'
|
|
|
|
|
|
def test_anthropic_shape_total_computed():
|
|
u = extract_usage({"usage": {"input_tokens": 7, "output_tokens": 3}})
|
|
assert u["input_tokens"] == 7
|
|
assert u["output_tokens"] == 3
|
|
assert u["total_tokens"] == 10 # точная сумма отчётных значений, не оценка
|
|
|
|
|
|
def test_camel_case_shape():
|
|
u = extract_usage({"usage": {"promptTokens": 4, "completionTokens": 2, "totalTokens": 6}})
|
|
assert (u["input_tokens"], u["output_tokens"], u["total_tokens"]) == (4, 2, 6)
|
|
|
|
|
|
def test_missing_usage_all_none():
|
|
assert extract_usage({"choices": []}) == EMPTY
|
|
assert extract_usage({"usage": None}) == EMPTY
|
|
assert extract_usage({}) == EMPTY
|
|
|
|
|
|
def test_non_dict_resp():
|
|
assert extract_usage(None) == EMPTY
|
|
assert extract_usage("error") == EMPTY
|
|
|
|
|
|
def test_zero_usage_treated_as_unknown():
|
|
# полностью нулевой usage = провайдер не отдал данные → NULL, не нули
|
|
u = extract_usage({"usage": {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0}})
|
|
assert u == EMPTY
|
|
|
|
|
|
def test_string_encoded_usage():
|
|
u = extract_usage({"usage": '{"prompt_tokens": 8, "completion_tokens": 1, "total_tokens": 9}'})
|
|
assert (u["input_tokens"], u["output_tokens"], u["total_tokens"]) == (8, 1, 9)
|
|
|
|
|
|
def test_partial_total_only():
|
|
u = extract_usage({"usage": {"total_tokens": 42}})
|
|
assert u["input_tokens"] is None
|
|
assert u["output_tokens"] is None
|
|
assert u["total_tokens"] == 42
|
|
|
|
|
|
def test_garbage_values_ignored():
|
|
u = extract_usage({"usage": {"prompt_tokens": "много", "completion_tokens": True, "total_tokens": None}})
|
|
assert u == EMPTY
|
|
|
|
|
|
def test_usage_json_roundtrip():
|
|
raw = {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15, "extra": {"x": 1}}
|
|
u = extract_usage({"usage": raw})
|
|
import json
|
|
assert json.loads(u["usage_json"]) == raw
|