|
- #!/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")
|