Skip to content
Closed
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
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ AI_VIDEO_FALLBACKS=kling-v2-6
# veo3.1 只对逗号分隔的这些用户 id 开放(组内做高质量素材用,不面向客户)。
# 空 = 谁都不能用。它按秒计费且比 kling 贵,且**永远不进兜底链**,只能由请求显式指定。
AI_VIDEO_VEO_USER_IDS=
# Agnes Video 2.5 走独立国际站;密钥仅配置在服务端环境变量中。
AI_VIDEO_AGNES_BASE_URL=https://apihub.agnes-ai.com/v1
AI_VIDEO_AGNES_API_KEY=
# 留空即按型号查上游牌价(美元);配了它就整条链共用这一个价,跨型号时会算错。
AI_IMAGE_UNIT_COST=
AI_VIDEO_UNIT_COST_PER_SECOND=
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ class AIProviderSettings(BaseSettings):
# 用途是组内做高质量素材,不面向客户。空值 = 没有任何人可用(默认)。
# 逗号分隔的用户 id,如 "1,7,12"。
video_veo_user_ids: str = ""
# Agnes 使用独立国际站凭证,不复用 Modelink 的全局 base_url/api_key。
# Key 只从部署环境读取;为空时 provider 会在上传首帧和发请求之前拒绝建单。
video_agnes_base_url: str = "https://apihub.agnes-ai.com/v1"
video_agnes_api_key: str = ""

chat_fallbacks: str = ""
image_fallbacks: str = "gemini-3.1-flash-image-preview"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
"kling-v2-6": Family.VIDEO_INPUT_REFERENCE,
"kling-video-o1": Family.VIDEO_IMAGE_LIST, # 登记但不允许进 chain
"veo3.1": Family.VIDEO_FAL_QUEUE, # 登记≠放行,还要在 USER_GATED_MODELS 的白名单里
"agnes-video-2.5": Family.VIDEO_AGNES,
"agnes-video-2.5-flash": Family.VIDEO_AGNES,
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class Family(str, Enum):
VIDEO_INPUT_REFERENCE = "video.input_reference"
VIDEO_IMAGE_LIST = "video.image_list"
VIDEO_FAL_QUEUE = "video.fal_queue"
VIDEO_AGNES = "video.agnes"


#: 每个 scene 允许出现哪些 family。链上混不同 family 是合法的 —— 兜底型号与主型号
Expand All @@ -34,6 +35,7 @@ class Family(str, Enum):
Scene.CHARACTER_ACTION: frozenset({
Family.VIDEO_INPUT_REFERENCE,
Family.VIDEO_FAL_QUEUE,
Family.VIDEO_AGNES,
}),
}

Expand Down
90 changes: 42 additions & 48 deletions backend/packages/framework/src/windup_framework/gateway/video.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
hash_bytes,
hash_image_input,
)
from windup_framework.gateway.types import AdapterResult, NextStep, Scene
from windup_framework.gateway.types import AdapterResult, Family, NextStep, Scene

_DEFAULT_RETRY_AFTER_S = 2.0
_SLEEP_CAP_S = 30.0
Expand Down Expand Up @@ -60,6 +60,20 @@ def __init__(self, registry, adapter, circuit, settings, route_adapters=None) ->
def _adapter_for(self, route: GatewayRoute):
return lookup_adapter(self._route_adapters, route, self._adapter)

def _open_circuit_scope(self, route: GatewayRoute, model: str) -> str | None:
"""返回会阻止本次型号提交的熔断层;Agnes 不受 Modelink 共享层影响。"""
isolated = self._registry.family_of(model) is Family.VIDEO_AGNES
if not isolated:
if self._circuit.is_open("aggregator"):
return "aggregator"
if self._circuit.is_open("base_url:" + route.base_url_id):
return "base_url"
if self._circuit.is_open(key_circuit_id(route)):
return "key"
if self._circuit.is_open("model:" + model):
return "model"
return None

def start_i2v(
self,
first_frame: bytes,
Expand Down Expand Up @@ -95,7 +109,9 @@ def poll_i2v(
snap = adapter.inspect_job(job_id, model=model)
if snap.ok and snap.job_status == "completed" and snap.edge_fingerprint:
if hasattr(adapter, "download_completed"):
return adapter.download_completed(job_id, snap.edge_fingerprint)
return adapter.download_completed(
job_id, snap.edge_fingerprint, model=model
)
return snap
return adapter.follow_job(job_id, model=model)

Expand Down Expand Up @@ -139,50 +155,13 @@ def fail(http_status: int | None) -> None:
f"http_status={http_status} error_type={err}"
)

if self._circuit.is_open("aggregator"):
model = models[0] if models else ""
route = routes[0]
self._emit(
AttemptTrace(
request_id=request_id,
scene=Scene.CHARACTER_ACTION,
model=model,
route=route,
attempt_index=seq.next_index(),
retry_count=0,
route_reason="skip_circuit_open",
outcome="failed",
circuit_scope="aggregator",
total_latency_ms=total_ms(),
maybe_billed=False,
detail=AttemptDetail(
input_hash=input_hash,
policy_next_step="fail",
upstream_reached="false",
),
)
)
fail(None)

for route_index, route in enumerate(routes):
if self._circuit.is_open("base_url:" + route.base_url_id):
if route_index + 1 < len(routes):
fallback_used = True
route_reason_override = "base_url_unreached"
continue
fail(last_http_status)
if self._circuit.is_open(key_circuit_id(route)):
if route_index + 1 < len(routes):
fallback_used = True
route_reason_override = "key_rate_limit"
continue
fail(last_http_status)

adapter = self._adapter_for(route)
switch_to_next_route = False
for i, model in enumerate(models):
model_index = start_i + i
if self._circuit.is_open("model:" + model):
open_scope = self._open_circuit_scope(route, model)
if open_scope is not None:
fallback_used = True
fallback_reason = "skip"
self._emit(
Expand All @@ -195,13 +174,15 @@ def fail(http_status: int | None) -> None:
retry_count=0,
route_reason="skip_circuit_open",
outcome="failed",
circuit_scope="model",
circuit_scope=open_scope,
fallback_used=fallback_used,
total_latency_ms=total_ms(),
maybe_billed=False,
detail=AttemptDetail(
input_hash=input_hash,
policy_next_step="fallback",
policy_next_step=(
"fallback" if open_scope == "model" else "fail"
),
upstream_reached="false",
model_index=model_index,
),
Expand Down Expand Up @@ -331,6 +312,17 @@ def fail(http_status: int | None) -> None:
has_job_id=has_job_id,
)
has_next_route = route_index + 1 < len(routes)
has_next_model = i + 1 < len(models)
uses_isolated_model_credentials = (
self._registry.family_of(model) is Family.VIDEO_AGNES
)
Comment on lines +316 to +318

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] 在路由熔断前放行独立的 Agnes 提交

这里仅在 Agnes 已经进入型号循环、提交失败后才把 OPEN_AGGREGATOR/FALLBACK_KEY 改为型号降级,但 i2v() 在此之前会先检查 aggregatorbase_url:<Modelink route> 和 key 熔断并直接失败或跳过整条 route。生产装配复用共享 _CIRCUIT,因此一次 Kling/Modelink 未达上游打开 base_url:qnaigc-primary(或全局 aggregator)后,即使 Agnes 使用完全独立的 URL/key,后续以 Agnes 为主模型的请求也不会调用 Agnes adapter;单 route 配置下会直接报错。这破坏了本 PR 的凭证/服务隔离和主模型可用性,应让 Agnes 绕过 Modelink route/key/aggregator 熔断,或为 Agnes 建立独立 route/circuit scope。

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已在 1a116e7 修复:将 aggregator、base_url 与 key 的熔断判定下沉到具体型号层;Agnes 仅绕过 Modelink 共享熔断,仍受自身 model 熔断约束,Kling 等型号继续遵守原共享熔断。新增三种共享 scope 的放行测试,并验证 Agnes 失败后不会绕过熔断提交 Kling。完整后端测试 1850 passed、14 skipped,ruff、import-linter 与 diff check 均通过。

if (
uses_isolated_model_credentials
and bound_job_id is None
and has_next_model
and step in (NextStep.OPEN_AGGREGATOR, NextStep.FALLBACK_KEY)
):
step = NextStep.FALLBACK
if step is NextStep.FAIL and bound_job_id is None:
tier_step = budget.tier_b_escalation(
error_type,
Expand Down Expand Up @@ -418,13 +410,15 @@ def fail(http_status: int | None) -> None:
and error_type is not ModelErrorType.UPSTREAM_FAILED
):
fail(last_http_status)
allowed_pre_submit_fallbacks = {
ModelErrorType.RATE_LIMIT,
ModelErrorType.MODEL_NOT_FOUND,
}
if uses_isolated_model_credentials:
allowed_pre_submit_fallbacks.add(ModelErrorType.UNREACHED)
if (
bound_job_id is None
and error_type
not in (
ModelErrorType.RATE_LIMIT,
ModelErrorType.MODEL_NOT_FOUND,
)
and error_type not in allowed_pre_submit_fallbacks
):
fail(last_http_status)
fallback_used = True
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
from .agnes_video import (
AGNES_VIDEO_25,
AGNES_VIDEO_25_FLASH,
AGNES_VIDEO_MODELS,
AgnesVideoProtocol,
)
from .fal_queue import (
FAL_I2V_ENDPOINTS,
FalQueueVideoProtocol,
Expand All @@ -15,6 +21,10 @@
from .types import HttpCall, JobProtocol, VideoRequest

__all__ = [
"AGNES_VIDEO_25",
"AGNES_VIDEO_25_FLASH",
"AGNES_VIDEO_MODELS",
"AgnesVideoProtocol",
"FAL_I2V_ENDPOINTS",
"FAL_IMAGE_ENDPOINTS",
"IMAGE_LIST_MODELS",
Expand Down
Loading
Loading