feat: 接入模型动态 Fallback 与调用审计

This commit is contained in:
hh
2026-07-21 10:35:50 +08:00
parent 9429133d04
commit d0c3690d47
45 changed files with 9253 additions and 173 deletions
+74 -20
View File
@@ -20,6 +20,7 @@ from __future__ import annotations
import json
import re
from decimal import Decimal
from functools import lru_cache
from pathlib import Path
@@ -574,7 +575,7 @@ def stream_script_agent(
):
"""生成 SSE 帧字符串的同步生成器,供 StreamingHttpResponse 包裹。
target_index 非空 = 精准只改第 N 镜(读全脚本上下文,后端强制保留其余镜原样)。"""
from apps.ai.services import build_provider, create_ai_task
from apps.ai.services import create_ai_task, stream_routed_text_request
yield _sse({"type": "tool", "id": "skill", "label": "加载电商脚本技能", "status": "running"})
skill_loaded = bool(load_ecommerce_skill())
@@ -619,6 +620,9 @@ def stream_script_agent(
"mode": mode,
"aspect_ratio": aspect_ratio,
"total_duration": total_duration,
"base_version_id": str(base_version_id or ""),
"target_index": target_index,
"model_routing_v1": True,
},
)
except Exception as exc: # noqa: BLE001 — 多为额度不足
@@ -639,13 +643,45 @@ def stream_script_agent(
task.status = AITask.Status.SUBMITTED
task.submitted_at = timezone.now()
task.save(update_fields=["status", "submitted_at", "updated_at"])
provider = build_provider(model_config)
for ev in provider.chat_completion_stream(
model=model_config.name,
endpoint=model_config.endpoint,
def validate_script_text(raw_text: str) -> dict:
candidate = normalize_draft(
raw_text,
aspect_ratio=aspect_ratio,
total_duration=effective_duration,
)
if target_index is not None and base_draft:
return _merge_single_segment(
base_draft,
candidate,
target_index,
aspect_ratio,
effective_duration,
)
return candidate
routed_stream = stream_routed_text_request(
task=task,
primary_model=model_config,
messages=messages,
streaming=True,
structured_output=True,
business_operation="script_generate",
temperature=0.85,
):
validate_text=validate_script_text,
request_summary={
"mode": mode,
"target_index": target_index,
"base_version_id": str(base_version_id or ""),
"aspect_ratio": aspect_ratio,
"total_duration": effective_duration,
},
)
while True:
try:
ev = next(routed_stream)
except StopIteration as completed:
routed = completed.value
break
et = ev.get("type")
if et == "reasoning":
# 思考流:推理模型在出 JSON 前会先想很久,把思考逐字下发给前端(像对话一样可见),
@@ -668,12 +704,8 @@ def stream_script_agent(
if piece.strip():
yield _sse({"type": "delta", "text": piece})
elif et == "done":
break
raw = "".join(full)
draft = normalize_draft(raw, aspect_ratio=aspect_ratio, total_duration=effective_duration)
if target_index is not None and base_draft:
# 精准改一镜:只采用新稿的第 target_index 镜,其余镜强制保持基准稿原样
draft = _merge_single_segment(base_draft, draft, target_index, aspect_ratio, effective_duration)
continue
raw, _provider_response, draft = routed.value
except Exception as exc: # noqa: BLE001
_fail_task(task, reservation, str(exc))
settled = True
@@ -819,7 +851,7 @@ def regenerate_segment_via_agent(*, project, user, model_config: ModelConfig, se
与 stream_script_agent 的 target_index 分支同源,但同步返回(不走 SSE)。计费 reserve→charge/release 闭环。"""
from django.db import transaction
from apps.ai.services import build_provider, create_ai_task
from apps.ai.services import create_ai_task, execute_routed_text_request
from apps.billing.services.ledger import charge_reserved_credit
base_draft = _draft_from_version(segment.script_version) # 用 DB 行重建基准,别用可能 stale 的 content
@@ -850,18 +882,40 @@ def regenerate_segment_via_agent(*, project, user, model_config: ModelConfig, se
"endpoint": model_config.endpoint,
"mode": "revise",
"target_index": target_index,
"model_routing_v1": True,
},
)
reservation = task.credit_reservation
# 每条真实尝试的平台成本由统一执行器累计;用户积分仍只结算这一条脚本任务。
task.base_cost = Decimal("0")
task.save(update_fields=["base_cost", "updated_at"])
# 实际平台成本由每条 AIModelAttempt 累加;用户积分仍只结算这一条逻辑任务。
task.base_cost = Decimal("0")
task.save(update_fields=["base_cost", "updated_at"])
try:
task.status = AITask.Status.SUBMITTED
task.submitted_at = timezone.now()
task.save(update_fields=["status", "submitted_at", "updated_at"])
provider = build_provider(model_config)
response = provider.chat_completion(model=model_config.name, endpoint=model_config.endpoint, messages=messages)
raw = provider.extract_text(response)
draft = normalize_draft(raw, aspect_ratio=aspect_ratio, total_duration=total_duration)
draft = _merge_single_segment(base_draft, draft, target_index, aspect_ratio, total_duration)
def validate_segment_text(raw_text: str) -> dict:
candidate = normalize_draft(raw_text, aspect_ratio=aspect_ratio, total_duration=total_duration)
return _merge_single_segment(base_draft, candidate, target_index, aspect_ratio, total_duration)
routed = execute_routed_text_request(
task=task,
primary_model=model_config,
messages=messages,
streaming=False,
structured_output=True,
business_operation="script_generate",
temperature=0.3,
validate_text=validate_segment_text,
request_summary={
"mode": "revise",
"target_index": target_index,
"base_version_id": str(segment.script_version_id),
},
)
raw, _response, draft = routed.value
with transaction.atomic():
task.status = AITask.Status.SUCCEEDED
task.response_payload = {"raw": raw[:8000]}
@@ -871,6 +925,6 @@ def regenerate_segment_via_agent(*, project, user, model_config: ModelConfig, se
charge_reserved_credit(reservation=reservation, actual_amount=task.actual_cost)
script = persist_script_draft(project=project, user=user, task=task, draft=draft, source="revise")
return script
except Exception:
_fail_task(task, reservation, "单镜重跑失败")
except Exception as exc:
_fail_task(task, reservation, str(exc) or "单镜重跑失败")
raise