fix(usage): 修复含失败信号的 pending 请求被误判为活跃的问题

- pending/streaming 状态含 status_code >= 400 或 error_message 时视为 failed
- 候选状态派生改为 failed 优先于 stale pending/streaming
- 活跃请求轮询时保留失败信号字段的更新
- 图片流支持 partial_image 事件转发及从 response.completed 读取最终图片
This commit is contained in:
fawney19
2026-04-24 03:07:11 +08:00
parent 581bc03d4e
commit 5148370253
16 changed files with 394 additions and 40 deletions
@@ -79,6 +79,17 @@ describe('usage status helpers', () => {
}))).toBe('streaming')
})
it('treats active lifecycle records with failure signals as failed for display', () => {
const record = buildUsageRecord({
status: 'pending',
status_code: 503,
error_message: 'upstream failed',
})
expect(resolveDisplayRequestStatus(record)).toBe('failed')
expect(isUsageRecordFailed(record)).toBe(true)
})
it('treats explicit success status code as authoritative for the timeline', () => {
expect(resolveTimelineFinalStatus({
traceFinalStatus: 'success',
@@ -93,6 +104,13 @@ describe('usage status helpers', () => {
})).toBe('failed')
})
it('does not let stale pending candidates override terminal request status', () => {
expect(resolveTimelineFinalStatus({
hasPendingCandidates: true,
requestStatus: 'failed',
})).toBe('failed')
})
it('uses explicit has_fallback flag for transfer filtering', () => {
expect(hasUsageFallback(buildUsageRecord({ has_fallback: true }))).toBe(true)
expect(hasUsageFallback(buildUsageRecord({ has_fallback: false }))).toBe(false)
+35 -11
View File
@@ -4,8 +4,18 @@ export type TimelineFinalStatus = 'success' | 'failed' | 'streaming' | 'pending'
type RequestStatusLike = RequestStatus | string | null | undefined
type UsageFailureSignal = {
status_code?: number | null
error_message?: string | null
}
type UsageDisplayStatusRecord = UsageFailureSignal & {
status?: RequestStatusLike
first_byte_time_ms?: number | null
}
function hasLegacyFailureSignal(
record: Pick<UsageRecord, 'status_code' | 'error_message'>
record: UsageFailureSignal
): boolean {
return (typeof record.status_code === 'number' && record.status_code >= 400) ||
(typeof record.error_message === 'string' && record.error_message.trim().length > 0)
@@ -138,7 +148,7 @@ function usageApiFormatDefaultsToNonStream(apiFormat: string): boolean {
}
function hasTerminalSuccessStatusCode(
record: Pick<UsageRecord, 'status_code'>
record: UsageFailureSignal
): boolean {
return typeof record.status_code === 'number' &&
record.status_code >= 200 &&
@@ -150,7 +160,10 @@ export function isUsageRecordFailed(
): boolean {
const status = typeof record.status === 'string' ? record.status.trim().toLowerCase() : ''
if (status) {
if (status === 'pending' || status === 'streaming' || status === 'cancelled') {
if (status === 'pending' || status === 'streaming') {
return !hasTerminalSuccessStatusCode(record) && hasLegacyFailureSignal(record)
}
if (status === 'cancelled') {
return false
}
if (status === 'completed') {
@@ -202,10 +215,13 @@ export function normalizeRequestStatus(status: RequestStatusLike): RequestStatus
}
}
export function resolveDisplayRequestStatus(
record: Pick<UsageRecord, 'status' | 'first_byte_time_ms'>
): RequestStatus | undefined {
export function resolveDisplayRequestStatus(record: UsageDisplayStatusRecord): RequestStatus | undefined {
const status = normalizeRequestStatus(record.status)
if ((status === 'pending' || status === 'streaming') &&
!hasTerminalSuccessStatusCode(record) &&
hasLegacyFailureSignal(record)) {
return 'failed'
}
if (status === 'streaming' && record.first_byte_time_ms == null) {
return 'pending'
}
@@ -251,20 +267,28 @@ export function resolveTimelineFinalStatus(params: {
requestStatus?: RequestStatusLike
statusCode?: number
}): TimelineFinalStatus {
if (params.hasPendingCandidates) {
return 'pending'
}
if (typeof params.statusCode === 'number') {
return params.statusCode >= 200 && params.statusCode < 400 ? 'success' : 'failed'
}
const traceStatus = normalizeTimelineFinalStatus(params.traceFinalStatus)
if (traceStatus) {
if (traceStatus === 'success' || traceStatus === 'failed' || traceStatus === 'cancelled') {
return traceStatus
}
const requestStatus = mapRequestStatusToTimelineStatus(params.requestStatus)
if (requestStatus === 'success' || requestStatus === 'failed' || requestStatus === 'cancelled') {
return requestStatus
}
if (params.hasPendingCandidates) {
return 'pending'
}
if (traceStatus) {
return traceStatus
}
if (requestStatus) {
return requestStatus
}