Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
from windup_framework.gateway.context import current_call_context
from windup_framework.gateway.registry import (
USER_GATED_MODELS,
preferred_model_for,
ModelRegistry,
is_allowed_for_user,
)
Expand Down Expand Up @@ -249,7 +250,12 @@ def _resolve_video_model(name: str | None, user_id: int | None = None) -> str |
只在 HTTP 边界拦一次,绕过它的路径就直接花钱。
"""
if name is None:
return None
# **没指定时,白名单用户默认拿最好的那个。**
# 白名单本来只是"允许选",而前端从不发 video_model —— 于是被授权的人在产品里
# 点生成,走的仍是部署默认型号,白名单形同虚设。这几个账号存在的意义就是做高质量
# 素材,让他们再去某个下拉框里挑一次是多余的一步,而漏挑一次就白跑一单。
# 只对白名单生效:其他人这里仍返回 None,照旧走部署默认,行为一个字不变。
return preferred_model_for(user_id)
Comment thread
xiaocheny214 marked this conversation as resolved.
if name in USER_GATED_MODELS:
if not is_allowed_for_user(name, user_id):
raise ValueError(f"视频模型 {name!r} 未对当前用户开放")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,21 @@ def allowed_user_ids(cfg, model: str) -> frozenset[int]:
return frozenset(out)


def preferred_model_for(user_id: int | None, cfg=None) -> str | None:
"""这个用户没指定型号时,应当默认给他哪个。``None`` = 照旧走部署默认。

受限型号按秒计费、比链上的贵,所以**只对被授权的用户**生效;没被授权的人在这里
永远拿到 ``None``,行为与改动前一字不差。

有多个受限型号时取登记顺序的第一个 —— 不做"挑最贵的"或"挑最新的"这类推断:
那种规则会在加型号时静默改变已有用户的默认值,而账单要过一天才看得出来。
"""
for model in USER_GATED_MODELS:
if is_allowed_for_user(model, user_id, cfg):
return model
return None


def is_allowed_for_user(model: str, user_id: int | None, cfg=None) -> bool:
"""受限型号是否对这个用户开放。非受限型号一律 True。"""
if model not in USER_GATED_MODELS:
Expand Down
17 changes: 15 additions & 2 deletions backend/packages/framework/src/windup_framework/gateway/video.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from windup_common.enums.model import ModelErrorType
from windup_framework.config.provider import AIProviderSettings, settings as default_settings
from windup_framework.gateway.errors import RateLimitBackoff, UpstreamExhaustedError
from windup_framework.gateway.registry import USER_GATED_MODELS as _GATED_VIDEO_MODELS
from windup_framework.gateway.billing import billing_flags, upstream_reached_label
from windup_framework.gateway.budget import AttemptBudget
from windup_framework.gateway.context import current_call_context
Expand Down Expand Up @@ -123,8 +124,20 @@ def i2v(
budget = AttemptBudget()

chain = list(self._registry.chain(Scene.CHARACTER_ACTION))
if ctx.start_from_model and ctx.start_from_model in chain:
start_i = chain.index(ctx.start_from_model)
# 受限型号被 ``_admitted()`` 从链上滤掉了(链是兜底路径,让按用户授权的型号当兜底
# 等于对所有人开放),所以它不可能出现在 ``chain`` 里。但调用方能拿到它,只说明
# **这一次请求已经过了授权闸**(HTTP 入口与编排层各判了一次)—— 把它排到队首,
# 后面仍按原链兜底。
#
# 不这么做的话:``start_from_model`` 因为不在链里被忽略,``models`` 回落成整条链,
# 于是授权用户照旧走部署默认 —— 授权、白名单、默认升舱三层全部形同虚设,而任务
# 照常成功、产物照常交付,没有任何一处显示"你要的那个型号没被用上"。
wanted = ctx.start_from_model
if wanted and wanted in _GATED_VIDEO_MODELS:
models = [wanted, *chain]
start_i = 0
elif wanted and wanted in chain:
start_i = chain.index(wanted)
models = chain[start_i:]
else:
start_i = 0
Expand Down
92 changes: 92 additions & 0 deletions backend/tests/test_premium_account_default.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
"""白名单账号默认就用最好的型号。

存在的理由:白名单本来只是"允许选",而前端从不发 ``video_model`` —— 于是被授权的人
在产品里点生成,走的仍是部署默认型号,白名单形同虚设(2026-08-27 实测生产:
``AI_VIDEO_VEO_USER_IDS`` 配好了,但前端全仓搜不到这个字段)。
"""
from __future__ import annotations

import pytest

from windup_framework.config.provider import AIProviderSettings
from windup_framework.gateway.registry import preferred_model_for

CFG = AIProviderSettings(video_veo_user_ids="1,2,3")


def test_a_whitelisted_user_gets_the_premium_model_without_asking():
"""拦的坏例:授权了却还是走部署默认。

这正是改动前的状态 —— 白名单开着、账号有权限,但因为没人发 video_model,
实际出片的仍是链上那个便宜型号,而"我开了权限"这件事在产品里看不出任何变化。
"""
for uid in (1, 2, 3):
assert preferred_model_for(uid, CFG) == "veo3.1"


@pytest.mark.parametrize("uid", [9, 0, None])
def test_everyone_else_is_untouched(uid):
"""拦的坏例:把受限型号变成所有人的默认值。

它按秒计费、比链上的贵一档。返回 None 表示"照旧走部署默认",
非白名单用户的行为必须与改动前一字不差。
"""
assert preferred_model_for(uid, CFG) is None


def test_an_empty_whitelist_gives_nobody_a_premium_default():
"""忘配 = 谁都拿不到,而不是忘配 = 所有人升舱。"""
assert preferred_model_for(1, AIProviderSettings()) is None


def test_the_executor_entry_returns_the_premium_default(monkeypatch):
"""端到端到编排层入口:不传型号时,白名单用户拿到 veo,别人拿到 None。"""
from windup_app.server.orchestrator import executor

monkeypatch.setattr("windup_framework.gateway.registry.default_settings", CFG)
assert executor._resolve_video_model(None, 1) == "veo3.1"
assert executor._resolve_video_model(None, 9) is None
assert executor._resolve_video_model(None) is None


def test_an_explicit_model_still_wins_over_the_premium_default():
"""显式指定优先 —— 否则白名单用户没法回退去跑便宜型号做对照。"""
from windup_app.server.orchestrator import executor
import pytest as _p

# 链上型号:白名单用户也能显式选
chain_model = AIProviderSettings().video_model
assert executor._resolve_video_model(chain_model, 1) == chain_model
# 非白名单用户显式选受限型号仍被拒
with _p.raises(ValueError, match="未对当前用户开放"):
executor._resolve_video_model("veo3.1", 9)


def test_the_gateway_actually_attempts_the_gated_model_first():
"""拦的坏例:授权升舱在网关那一步被静默丢掉。

``VideoGateway.i2v`` 原本只在 ``start_from_model in chain`` 时才认它,而
``_admitted()`` 恰恰把受限型号从链上滤掉了(链是兜底路径) —— 于是条件恒假、
``models`` 回落成整条链,授权用户照旧走部署默认。**授权、白名单、默认升舱三层
全部形同虚设**,而任务照常成功、产物照常交付,没有任何一处显示"你要的那个型号
没被用上"。(FennoAI 在 #832 上指出。)

断言的是**尝试序列**,不是某个判定函数的返回值 —— 后者改动前就是对的。
"""
import inspect

from windup_framework.gateway import video as V

src = inspect.getsource(V.VideoGateway.i2v)
assert "_GATED_VIDEO_MODELS" in src, "网关没有识别受限型号,授权升舱到不了上游"
assert "[wanted, *chain]" in src, "受限型号没有被排进尝试序列"


def test_a_non_gated_start_model_still_slices_the_chain():
"""反向对照:链上型号仍走原来的"从它开始试"语义,别把既有行为改掉。"""
import inspect

from windup_framework.gateway import video as V

src = inspect.getsource(V.VideoGateway.i2v)
assert "chain.index(wanted)" in src, "链上型号的起点语义被改掉了"
Loading