feat: 视频计费增强与影子计费系统

This commit is contained in:
fawney19
2026-02-03 18:48:39 +08:00
parent ed68aebfb0
commit 00442c41fa
84 changed files with 6507 additions and 1678 deletions

View File

@@ -100,10 +100,7 @@ class StreamTelemetryRecorder:
return
actual_request_body = ctx.provider_request_body or original_request_body
response_body = None
if (
not isinstance(writer, QueueTelemetryWriter)
or config.usage_queue_include_bodies
):
if not isinstance(writer, QueueTelemetryWriter) or writer.include_bodies:
response_body = ctx.build_response_body(response_time_ms)
try:
@@ -403,10 +400,25 @@ class StreamTelemetryRecorder:
self, bg_db: Session, ctx: StreamContext, response_time_ms: int
) -> TelemetryWriter | None:
if config.usage_queue_enabled and self.user_id and self.api_key_id:
from src.services.system.config import SystemConfigService
# Queue payload detail follows system config request_record_level.
log_level = SystemConfigService.get_request_record_level(bg_db).value
sensitive_headers = SystemConfigService.get_sensitive_headers(bg_db) or []
max_request_body_size = int(
SystemConfigService.get_config(bg_db, "max_request_body_size", 5242880) or 0
)
max_response_body_size = int(
SystemConfigService.get_config(bg_db, "max_response_body_size", 5242880) or 0
)
return QueueTelemetryWriter(
request_id=self.request_id,
user_id=self.user_id,
api_key_id=self.api_key_id,
log_level=log_level,
sensitive_headers=sensitive_headers,
max_request_body_size=max_request_body_size,
max_response_body_size=max_response_body_size,
)
db_writer = self._build_db_writer(bg_db)
if db_writer is None:

View File

@@ -75,6 +75,17 @@ class GeminiVeoHandler(VideoHandlerBase):
)
self._normalizer = GeminiNormalizer()
@staticmethod
def _get_request_base_url(http_request: Request) -> str:
"""从 HTTP 请求中获取基础 URL协议 + 主机)"""
# 优先使用 X-Forwarded-Proto 和 X-Forwarded-Host代理场景
proto = http_request.headers.get("x-forwarded-proto") or http_request.url.scheme
host = http_request.headers.get("x-forwarded-host") or http_request.headers.get("host")
if host:
return f"{proto}://{host}"
# 回退到 request.url
return f"{http_request.url.scheme}://{http_request.url.netloc}"
async def handle_create_task(
self,
*,
@@ -259,7 +270,8 @@ class GeminiVeoHandler(VideoHandlerBase):
created_at=task.created_at,
original_request=internal_request,
)
response_body = self._normalizer.video_task_from_internal(internal_task)
base_url = self._get_request_base_url(http_request)
response_body = self._normalizer.video_task_from_internal(internal_task, base_url=base_url)
# 提交成功后立即结算 Usage费用暂时为 0轮询完成后更新
response_time_ms = int((time.time() - self.start_time) * 1000)
@@ -313,7 +325,8 @@ class GeminiVeoHandler(VideoHandlerBase):
# 直接从数据库返回任务状态(后台轮询服务会持续更新状态)
internal_task = self._task_to_internal(task)
response_body = self._normalizer.video_task_from_internal(internal_task)
base_url = self._get_request_base_url(http_request)
response_body = self._normalizer.video_task_from_internal(internal_task, base_url=base_url)
return JSONResponse(response_body)
async def handle_list_tasks(
@@ -331,8 +344,10 @@ class GeminiVeoHandler(VideoHandlerBase):
.limit(100)
.all()
)
base_url = self._get_request_base_url(http_request)
items = [
self._normalizer.video_task_from_internal(self._task_to_internal(t)) for t in tasks
self._normalizer.video_task_from_internal(self._task_to_internal(t), base_url=base_url)
for t in tasks
]
return JSONResponse({"operations": items})

View File

@@ -10,6 +10,7 @@ from datetime import datetime, timedelta, timezone
from typing import Any, AsyncIterator
from uuid import uuid4
import httpx
from fastapi import HTTPException, Request
from fastapi.responses import JSONResponse, Response, StreamingResponse
from sqlalchemy.exc import IntegrityError
@@ -650,8 +651,10 @@ class OpenAIVideoHandler(VideoHandlerBase):
try:
# 使用 httpx 的 stream 方法并正确管理上下文
# 视频下载可能较大,设置 5 分钟超时
request = client.build_request("GET", upstream_url, headers=headers)
response = await client.send(request, stream=True, timeout=300.0)
request = client.build_request(
"GET", upstream_url, headers=headers, timeout=httpx.Timeout(300.0)
)
response = await client.send(request, stream=True)
except Exception as exc:
logger.warning(
"[VideoDownload] Upstream connection failed task={} url={}: {}",
@@ -723,8 +726,8 @@ class OpenAIVideoHandler(VideoHandlerBase):
"""代理直接的视频 URL如 CDN URL保持与官方 API 一致的流式返回行为"""
client = await HTTPClientPool.get_default_client_async()
try:
request = client.build_request("GET", url)
response = await client.send(request, stream=True, timeout=300.0)
request = client.build_request("GET", url, timeout=httpx.Timeout(300.0))
response = await client.send(request, stream=True)
except Exception as exc:
logger.warning(
"[VideoDownload] Direct URL connection failed task={} url={}: {}",