From 2be3a5fef48140e665bd6ef94abfb2bd89af7ae6 Mon Sep 17 00:00:00 2001 From: Johnny Zhang Date: Thu, 27 Aug 2026 17:04:54 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat(video):=20=E8=A2=AB=E6=8E=88=E6=9D=83?= =?UTF-8?q?=E7=9A=84=E8=B4=A6=E5=8F=B7=E9=BB=98=E8=AE=A4=E5=B0=B1=E7=94=A8?= =?UTF-8?q?=E9=AB=98=E8=B4=A8=E9=87=8F=E5=9E=8B=E5=8F=B7,=E4=B8=8D?= =?UTF-8?q?=E5=BF=85=E8=87=AA=E5=B7=B1=E5=8E=BB=E9=80=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 白名单本来只是"允许选",而前端从不发 video_model —— 2026-08-27 实测生产: AI_VIDEO_VEO_USER_IDS 已配好,但前端全仓搜不到这个字段,于是被授权的人在产品里点 生成,走的仍是部署默认型号,白名单形同虚设。 这几个账号存在的意义就是做高质量素材,让他们再去某个下拉框里挑一次是多余的一步, 而漏挑一次就白跑一单。只对白名单生效:其他人这里仍返回 None、照旧走部署默认, 行为一个字不变。 有多个受限型号时取登记顺序的第一个,不做"挑最贵的/最新的"这类推断 —— 那种规则会在 加型号时静默改变已有用户的默认值,而账单要过一天才看得出来。 Closes #830 --- .../server/orchestrator/executor.py | 8 ++- .../src/windup_framework/gateway/registry.py | 15 +++++ backend/tests/test_premium_account_default.py | 62 +++++++++++++++++++ 3 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 backend/tests/test_premium_account_default.py diff --git a/backend/packages/app/src/windup_app/server/orchestrator/executor.py b/backend/packages/app/src/windup_app/server/orchestrator/executor.py index f70a8f11..1f34179e 100644 --- a/backend/packages/app/src/windup_app/server/orchestrator/executor.py +++ b/backend/packages/app/src/windup_app/server/orchestrator/executor.py @@ -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, ) @@ -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) if name in USER_GATED_MODELS: if not is_allowed_for_user(name, user_id): raise ValueError(f"视频模型 {name!r} 未对当前用户开放") diff --git a/backend/packages/framework/src/windup_framework/gateway/registry.py b/backend/packages/framework/src/windup_framework/gateway/registry.py index 0ffe7238..7445f93a 100644 --- a/backend/packages/framework/src/windup_framework/gateway/registry.py +++ b/backend/packages/framework/src/windup_framework/gateway/registry.py @@ -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: diff --git a/backend/tests/test_premium_account_default.py b/backend/tests/test_premium_account_default.py new file mode 100644 index 00000000..3fe48019 --- /dev/null +++ b/backend/tests/test_premium_account_default.py @@ -0,0 +1,62 @@ +"""白名单账号默认就用最好的型号。 + +存在的理由:白名单本来只是"允许选",而前端从不发 ``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) From 33a62d68cffd46f537a7c9ff4889cc5e0d61c344 Mon Sep 17 00:00:00 2001 From: Johnny Zhang Date: Thu, 27 Aug 2026 19:07:39 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix(video):=20=E7=BD=91=E5=85=B3=E8=AE=A4?= =?UTF-8?q?=E6=8E=88=E6=9D=83=E5=9E=8B=E5=8F=B7,=E5=90=A6=E5=88=99?= =?UTF-8?q?=E9=BB=98=E8=AE=A4=E5=8D=87=E8=88=B1=E4=B8=80=E6=AD=A5=E9=83=BD?= =?UTF-8?q?=E4=B8=8D=E7=94=9F=E6=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FennoAI 指出:VideoGateway.i2v 只在 start_from_model in chain 时才认它,而 _admitted() 恰恰把受限型号从链上滤掉了(链是兜底路径) —— 条件恒假、models 回落成整条链,授权用户 照旧走部署默认。授权、白名单、默认升舱三层全部形同虚设,而任务照常成功、产物照常交付, 没有任何一处显示你要的那个型号没被用上。 调用方能拿到受限型号,只说明这一次请求已经过了授权闸(HTTP 入口与编排层各判了一次), 故把它排到尝试序列队首,后面仍按原链兜底。链上型号的从它开始试语义不变。 --- .../src/windup_framework/gateway/video.py | 17 +++++++++-- backend/tests/test_premium_account_default.py | 30 +++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/backend/packages/framework/src/windup_framework/gateway/video.py b/backend/packages/framework/src/windup_framework/gateway/video.py index d4788ec4..9e10202e 100644 --- a/backend/packages/framework/src/windup_framework/gateway/video.py +++ b/backend/packages/framework/src/windup_framework/gateway/video.py @@ -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 @@ -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 diff --git a/backend/tests/test_premium_account_default.py b/backend/tests/test_premium_account_default.py index 3fe48019..213ab45d 100644 --- a/backend/tests/test_premium_account_default.py +++ b/backend/tests/test_premium_account_default.py @@ -60,3 +60,33 @@ def test_an_explicit_model_still_wins_over_the_premium_default(): # 非白名单用户显式选受限型号仍被拒 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, "链上型号的起点语义被改掉了"