feat: 优化用量状态同步机制和 OpenAI CLI 流式转换

- 增加状态回退保护,防止异步响应覆盖已知状态
- 添加轮询并发保护 (pollInFlight),避免重复请求
- 支持 cancelled 状态筛选和显示
- 前端 mergeRecordStatus 保护活跃记录状态
- 后端 streaming 状态同步更新改为使用当前 DB 会话
- OpenAI CLI 流式转换增加工具调用事件支持
- 轮询接口新增 target_model 字段返回
- 修复迁移脚本 inspector 缓存问题,改用 information_schema
This commit is contained in:
fawney19
2026-02-04 03:17:55 +08:00
parent f3e2f84b38
commit 7f09f191a1
17 changed files with 957 additions and 84 deletions

View File

@@ -857,7 +857,7 @@ class AdminUsageRecordsAdapter(AdminApiAdapter):
query = query.filter(Usage.is_stream == False) # noqa: E712
elif self.status == "error":
query = query.filter((Usage.status_code >= 400) | (Usage.error_message.isnot(None)))
elif self.status in ("pending", "streaming", "completed"):
elif self.status in ("pending", "streaming", "completed", "cancelled"):
# 新的状态筛选:直接按 status 字段过滤
query = query.filter(Usage.status == self.status)
elif self.status == "failed":

View File

@@ -2676,7 +2676,28 @@ class CliMessageHandlerBase(BaseMessageHandler):
ctx.record_first_byte_time(self.start_time)
state["first_yield"] = False
if not state["streaming_updated"]:
self._update_usage_to_streaming_with_ctx(ctx)
# 优先使用当前请求的 DB 会话同步更新,避免状态延迟或丢失
try:
from src.services.usage import UsageService
UsageService.update_usage_status(
db=self.db,
request_id=self.request_id,
status="streaming",
provider=ctx.provider_name,
target_model=ctx.mapped_model,
provider_id=ctx.provider_id,
provider_endpoint_id=ctx.endpoint_id,
provider_api_key_id=ctx.key_id,
first_byte_time_ms=ctx.first_byte_time_ms,
api_format=ctx.api_format,
endpoint_api_format=ctx.provider_api_format or None,
has_format_conversion=ctx.needs_conversion,
)
except Exception as e:
logger.warning(f"[{self.request_id}] 同步更新 streaming 状态失败: {e}")
# 回退到后台任务更新
self._update_usage_to_streaming_with_ctx(ctx)
state["streaming_updated"] = True
def _convert_sse_line(

View File

@@ -112,6 +112,7 @@ class StreamTelemetryRecorder:
try:
await self._dispatch_record(
bg_db,
writer,
ctx,
original_headers,
@@ -137,6 +138,7 @@ class StreamTelemetryRecorder:
if response_body is None and should_log_body:
response_body = ctx.build_response_body(response_time_ms)
await self._dispatch_record(
bg_db,
db_writer,
ctx,
original_headers,
@@ -438,6 +440,7 @@ class StreamTelemetryRecorder:
async def _dispatch_record(
self,
db: Session,
writer: TelemetryWriter,
ctx: StreamContext,
original_headers: dict[str, str],
@@ -455,6 +458,14 @@ class StreamTelemetryRecorder:
response_body,
response_time_ms,
)
# Queue writer 异步落库可能造成 UI 延迟,先直接更新 Usage 状态
if isinstance(writer, QueueTelemetryWriter):
await self._update_usage_status_directly(
db=db,
status=self._get_status_from_ctx(ctx),
response_time_ms=response_time_ms,
status_code=ctx.status_code,
)
elif ctx.is_client_disconnected():
await self._record_cancelled(
writer,
@@ -464,6 +475,14 @@ class StreamTelemetryRecorder:
response_body,
response_time_ms,
)
# Queue writer 异步落库可能造成 UI 延迟,先直接更新 Usage 状态
if isinstance(writer, QueueTelemetryWriter):
await self._update_usage_status_directly(
db=db,
status="cancelled",
response_time_ms=response_time_ms,
status_code=ctx.status_code,
)
else:
await self._record_failure(
writer,
@@ -473,6 +492,15 @@ class StreamTelemetryRecorder:
response_body,
response_time_ms,
)
# Queue writer 异步落库可能造成 UI 延迟,先直接更新 Usage 状态
if isinstance(writer, QueueTelemetryWriter):
await self._update_usage_status_directly(
db=db,
status=self._get_status_from_ctx(ctx),
response_time_ms=response_time_ms,
status_code=ctx.status_code,
error_message=ctx.error_message or f"HTTP {ctx.status_code}",
)
def _get_status_from_ctx(self, ctx: StreamContext) -> str:
"""根据上下文获取状态字符串"""