#!/usr/bin/env python3 """ Smoke test for video pricing table billing. Required environment: NEW_API_BASE_URL=http://127.0.0.1:3000 NEW_API_ADMIN_USERNAME=root NEW_API_ADMIN_PASSWORD=123456 NEW_API_TOKEN=sk-... Optional model overrides: VIDEO_PRICING_HAILUO_MODEL=hailuo-video VIDEO_PRICING_SEEDANCE_MODEL=seedance-2 The server must already have usable channels for the selected models. For the per_1m_tokens case, point the Seedance/Doubao channel at a mock upstream that returns a completed task with usage.total_tokens. """ from __future__ import annotations import json import os import sys import time import urllib.error import urllib.parse import urllib.request from http.cookiejar import CookieJar BASE_URL = os.getenv("NEW_API_BASE_URL", "http://127.0.0.1:3000").rstrip("/") ADMIN_USERNAME = os.getenv("NEW_API_ADMIN_USERNAME", "root") ADMIN_PASSWORD = os.getenv("NEW_API_ADMIN_PASSWORD", "123456") API_TOKEN = os.getenv("NEW_API_TOKEN", "") HAILUO_MODEL = os.getenv("VIDEO_PRICING_HAILUO_MODEL", "hailuo-video") SEEDANCE_MODEL = os.getenv("VIDEO_PRICING_SEEDANCE_MODEL", "seedance-2") QUOTA_PER_UNIT = int(os.getenv("VIDEO_PRICING_QUOTA_PER_UNIT", "500000")) GROUP_RATIO = float(os.getenv("VIDEO_PRICING_GROUP_RATIO", "1")) class Client: def __init__(self) -> None: self.cookies = CookieJar() self.opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(self.cookies)) def request(self, method: str, path: str, body=None, token: str | None = None, expected: int | None = None): url = f"{BASE_URL}{path}" data = None headers = {"Content-Type": "application/json"} if body is not None: data = json.dumps(body).encode("utf-8") if token: headers["Authorization"] = f"Bearer {token}" req = urllib.request.Request(url, data=data, headers=headers, method=method) try: with self.opener.open(req, timeout=30) as resp: payload = read_json(resp) if expected is not None and resp.status != expected: raise AssertionError(f"{method} {path}: expected {expected}, got {resp.status}: {payload}") return resp.status, payload except urllib.error.HTTPError as err: payload = read_json(err) if expected is not None and err.code == expected: return err.code, payload raise AssertionError(f"{method} {path}: HTTP {err.code}: {payload}") from err def read_json(resp): raw = resp.read().decode("utf-8", errors="replace") if not raw: return None try: return json.loads(raw) except json.JSONDecodeError: return raw def assert_success(payload, context: str): if not isinstance(payload, dict) or payload.get("success") is not True: raise AssertionError(f"{context} failed: {payload}") return payload.get("data") def usage(client: Client) -> dict: _, payload = client.request("GET", "/api/usage/token/", token=API_TOKEN, expected=200) if not payload.get("code"): raise AssertionError(f"usage failed: {payload}") return payload["data"] def used_quota(client: Client) -> int: return int(usage(client)["total_used"]) def login(client: Client) -> None: _, payload = client.request( "POST", "/api/user/login", {"username": ADMIN_USERNAME, "password": ADMIN_PASSWORD}, expected=200, ) assert_success(payload, "admin login") def put_model_pricing(client: Client, model: str, config: dict) -> None: _, payload = client.request("PUT", f"/api/option/model_pricing/{urllib.parse.quote(model, safe='')}", config, expected=200) assert_success(payload, f"put pricing {model}") def delete_model_pricing(client: Client, model: str) -> None: _, payload = client.request("DELETE", f"/api/option/model_pricing/{urllib.parse.quote(model, safe='')}", expected=200) assert_success(payload, f"delete pricing {model}") def submit_video(client: Client, body: dict, expected: int = 200): _, payload = client.request("POST", "/v1/videos", body, token=API_TOKEN, expected=expected) return payload def fetch_video(client: Client, task_id: str): _, payload = client.request("GET", f"/v1/videos/{urllib.parse.quote(task_id, safe='')}", token=API_TOKEN, expected=200) return payload def hailuo_config() -> dict: return { "schema_version": 1, "scope": "model", "billing_unit": "per_call", "preconsume_strategy": "exact", "dimensions": [ {"key": "resolution", "source": "request.resolution", "type": "string", "default": None}, {"key": "duration", "source": "request.duration", "type": "number", "default": None}, ], "table": [ {"resolution": "768P", "duration": 10, "price": 0.04, "source": "manual"}, ], "fallback": {"strategy": "reject"}, } def seedance_config() -> dict: return { "schema_version": 1, "scope": "model", "billing_unit": "per_1m_tokens", "preconsume_strategy": "minimum", "dimensions": [ {"key": "resolution", "source": "request.resolution", "type": "string", "default": None}, ], "table": [ {"resolution": "720P", "price": 0.5, "source": "manual"}, ], "fallback": {"strategy": "reject"}, } def expected_quota(price_usd: float, tokens: int | None = None) -> int: units = tokens if tokens is not None else 1 divisor = 1_000_000 if tokens is not None else 1 quota = int(float(units) * price_usd / divisor * QUOTA_PER_UNIT * GROUP_RATIO) if tokens and quota <= 0 and price_usd > 0 and GROUP_RATIO > 0: return 1 return quota def run() -> None: if not API_TOKEN: raise RuntimeError("NEW_API_TOKEN is required") client = Client() login(client) try: put_model_pricing(client, HAILUO_MODEL, hailuo_config()) before = used_quota(client) payload = submit_video( client, {"model": HAILUO_MODEL, "prompt": "pricing smoke", "resolution": "768P", "duration": 10}, ) after = used_quota(client) assert after - before == expected_quota(0.04), f"Hailuo quota mismatch: before={before}, after={after}, response={payload}" before_reject = used_quota(client) rejected = submit_video( client, {"model": HAILUO_MODEL, "prompt": "pricing smoke", "resolution": "1080P", "duration": 10}, expected=400, ) after_reject = used_quota(client) assert after_reject == before_reject, f"reject changed quota: {before_reject} -> {after_reject}, response={rejected}" put_model_pricing(client, SEEDANCE_MODEL, seedance_config()) before_usage = used_quota(client) seedance_submit = submit_video( client, {"model": SEEDANCE_MODEL, "prompt": "pricing smoke", "resolution": "720P", "duration": 5}, ) task_id = seedance_submit.get("id") or seedance_submit.get("task_id") or seedance_submit.get("data", {}).get("id") if not task_id: raise AssertionError(f"Seedance submit did not return task id: {seedance_submit}") total_tokens = int(os.getenv("VIDEO_PRICING_EXPECTED_TOTAL_TOKENS", "1000")) for _ in range(12): fetched = fetch_video(client, task_id) status = fetched.get("status") or fetched.get("data", {}).get("status") if status in {"succeeded", "completed", "success"}: break time.sleep(1) after_usage = used_quota(client) delta = after_usage - before_usage assert delta == expected_quota(0.5, total_tokens), f"Seedance usage quota mismatch: delta={delta}, response={fetched}" finally: delete_model_pricing(client, HAILUO_MODEL) delete_model_pricing(client, SEEDANCE_MODEL) if __name__ == "__main__": try: run() except Exception as exc: print(f"FAIL: {exc}", file=sys.stderr) sys.exit(1) print("PASS: video pricing smoke completed")