[codex] fix empty stream success handling and unknown usage logs (#343)

* fix: preserve repeated short responses deltas

* fix empty stream success handling and unknown usage logs

* fix proxy usage metadata typing

* fix stream bookkeeping and explicit zero usage source

* fix sticky fast path and usage placeholder detection
This commit is contained in:
Cita
2026-03-31 16:45:13 +08:00
committed by GitHub
parent 3ce5179fed
commit 6b19d06140
24 changed files with 926 additions and 73 deletions
+17
View File
@@ -173,6 +173,23 @@ describe('proxyLogStore', () => {
});
});
it('preserves null token fields instead of coercing unknown usage to zero', async () => {
await insertProxyLog({
modelRequested: 'gpt-5',
promptTokens: null,
completionTokens: null,
totalTokens: null,
});
expect(dbInsertValuesMock).toHaveBeenCalledTimes(1);
expect(dbInsertValuesMock.mock.calls[0][0]).toMatchObject({
modelRequested: 'gpt-5',
promptTokens: null,
completionTokens: null,
totalTokens: null,
});
});
it('retries proxy log inserts without structured client fields when those columns are missing', async () => {
hasProxyLogClientColumnsMock.mockResolvedValue(true);
dbInsertRunMock
+3 -3
View File
@@ -214,9 +214,9 @@ export async function insertProxyLog(input: ProxyLogInsertInput): Promise<void>
status: input.status ?? null,
httpStatus: input.httpStatus ?? null,
latencyMs: input.latencyMs ?? null,
promptTokens: input.promptTokens ?? 0,
completionTokens: input.completionTokens ?? 0,
totalTokens: input.totalTokens ?? 0,
promptTokens: input.promptTokens ?? null,
completionTokens: input.completionTokens ?? null,
totalTokens: input.totalTokens ?? null,
estimatedCost: input.estimatedCost ?? 0,
errorMessage: input.errorMessage ?? null,
retryCount: input.retryCount ?? 0,
@@ -129,4 +129,36 @@ describe('proxyUsageFallbackService sub2api', () => {
estimatedCostFromQuota: 0.143004,
});
});
it('treats explicit zero upstream usage as upstream instead of unknown', async () => {
const result = await resolveProxyUsageWithSelfLogFallback({
site: {
url: 'https://newapi.example.com',
platform: 'new-api',
},
account: {
accessToken: 'jwt-access-token',
},
modelName: 'gpt-5.4',
requestStartedAtMs: Date.parse('2026-03-07T06:12:07.000Z'),
requestEndedAtMs: Date.parse('2026-03-07T06:12:30.000Z'),
localLatencyMs: 23000,
upstreamUsagePresent: true,
usage: {
promptTokens: 0,
completionTokens: 0,
totalTokens: 0,
},
});
expect(result).toMatchObject({
promptTokens: 0,
completionTokens: 0,
totalTokens: 0,
recoveredFromSelfLog: false,
usageSource: 'upstream',
});
expect(fetchMock).not.toHaveBeenCalled();
expect(fetchJsonWithShieldCookieRetryMock).not.toHaveBeenCalled();
});
});
@@ -44,6 +44,7 @@ interface ProxyUsageFallbackInput {
requestStartedAtMs: number;
requestEndedAtMs: number;
localLatencyMs: number;
upstreamUsagePresent?: boolean;
usage: ProxyUsage;
}
@@ -51,6 +52,7 @@ interface ProxyUsageFallbackResult extends ProxyUsage {
recoveredFromSelfLog: boolean;
estimatedCostFromQuota: number;
selfLogBillingMeta: SelfLogBillingMeta | null;
usageSource: 'upstream' | 'self-log' | 'unknown';
}
export interface SelfLogBillingMeta {
@@ -149,11 +151,14 @@ function isUsageMissing(usage: ProxyUsage): boolean {
export function shouldLookupSelfLog(
platform: string,
usage: ProxyUsage,
upstreamUsagePresent?: boolean,
): boolean {
const normalizedPlatform = String(platform || '').toLowerCase();
if (!SUPPORTED_USAGE_FALLBACK_PLATFORMS.has(normalizedPlatform)) return false;
if (ALWAYS_LOOKUP_SELF_LOG_PLATFORMS.has(normalizedPlatform)) return true;
return isUsageMissing(usage);
const normalizedUsage = normalizeUsage(usage);
const hasUpstreamUsage = upstreamUsagePresent ?? !isUsageMissing(normalizedUsage);
return !hasUpstreamUsage;
}
function normalizeUrl(url: string): string {
@@ -534,15 +539,18 @@ export async function resolveProxyUsageWithSelfLogFallback(
input: ProxyUsageFallbackInput,
): Promise<ProxyUsageFallbackResult> {
const normalizedUsage = normalizeUsage(input.usage);
const hasUpstreamUsage = input.upstreamUsagePresent ?? !isUsageMissing(normalizedUsage);
const fallbackUsageSource = hasUpstreamUsage ? 'upstream' : 'unknown';
const fallback: ProxyUsageFallbackResult = {
...normalizedUsage,
recoveredFromSelfLog: false,
estimatedCostFromQuota: 0,
selfLogBillingMeta: null,
usageSource: fallbackUsageSource,
};
const platform = String(input.site.platform || '').toLowerCase();
if (!shouldLookupSelfLog(platform, normalizedUsage)) {
if (!shouldLookupSelfLog(platform, normalizedUsage, hasUpstreamUsage)) {
return fallback;
}
@@ -577,6 +585,7 @@ export async function resolveProxyUsageWithSelfLogFallback(
recoveredFromSelfLog: true,
estimatedCostFromQuota: toRecoveredCost(matched),
selfLogBillingMeta: matched.billingMeta,
usageSource: useMatchedTokens ? 'self-log' : fallbackUsageSource,
};
} catch {
return fallback;
@@ -1,5 +1,6 @@
import { describe, expect, it } from 'vitest';
import {
hasProxyUsagePayload,
mergeProxyUsage,
parseProxyUsage,
pullSseDataEvents,
@@ -160,6 +161,30 @@ describe('proxyUsageParser', () => {
});
});
it('does not treat null placeholders as explicit upstream usage', () => {
expect(hasProxyUsagePayload({
usage: {
total_tokens: null,
cache_creation: {},
},
})).toBe(false);
});
it('treats explicit zero usage values as upstream usage', () => {
expect(hasProxyUsagePayload({
usage: {
total_tokens: 0,
},
})).toBe(true);
expect(hasProxyUsagePayload({
usage: {
prompt_tokens_details: {
cached_tokens: 0,
},
},
})).toBe(true);
});
it('pulls SSE data events across chunk boundaries', () => {
const first = pullSseDataEvents('data: {"a":1}\n\ndata: {"b":');
expect(first.events).toEqual(['{"a":1}']);
+81
View File
@@ -16,6 +16,60 @@ const ZERO_USAGE: ParsedProxyUsage = {
promptTokensIncludeCache: null,
};
const USAGE_DIRECT_KEYS = [
'prompt_tokens',
'promptTokens',
'prompt_token_count',
'promptTokenCount',
'input_tokens',
'inputTokens',
'input_token_count',
'inputTokenCount',
'completion_tokens',
'completionTokens',
'completion_token_count',
'completionTokenCount',
'candidates_token_count',
'candidatesTokenCount',
'output_tokens',
'outputTokens',
'output_token_count',
'outputTokenCount',
'total_tokens',
'totalTokens',
'total_token_count',
'totalTokenCount',
'cache_read_input_tokens',
'cacheReadInputTokens',
'prompt_cache_hit_tokens',
'promptCacheHitTokens',
'cached_tokens',
'cachedTokens',
'cache_read_tokens',
'cacheReadTokens',
'cache_creation_input_tokens',
'cacheCreationInputTokens',
'cache_creation_tokens',
'cacheCreationTokens',
'claude_cache_creation_5_m_tokens',
'claudeCacheCreation5mTokens',
'claude_cache_creation_1_h_tokens',
'claudeCacheCreation1hTokens',
] as const;
const USAGE_DETAIL_KEYS = [
'prompt_tokens_details',
'promptTokensDetails',
'input_tokens_details',
'inputTokensDetails',
'completion_tokens_details',
'completionTokensDetails',
'output_tokens_details',
'outputTokensDetails',
'cache_creation',
'cacheCreation',
] as const;
function toPositiveInt(value: unknown): number {
const n = typeof value === 'number' ? value : Number(value);
if (!Number.isFinite(n)) return 0;
@@ -26,6 +80,24 @@ function isRecord(value: unknown): value is Record<string, unknown> {
return !!value && typeof value === 'object';
}
function hasOwn(record: Record<string, unknown>, key: string): boolean {
return Object.prototype.hasOwnProperty.call(record, key);
}
function hasExplicitUsageValue(value: unknown): boolean {
if (value === null || value === undefined) return false;
if (typeof value === 'number') return Number.isFinite(value);
if (typeof value === 'string') {
const trimmed = value.trim();
return trimmed.length > 0 && Number.isFinite(Number(trimmed));
}
if (Array.isArray(value)) {
return value.some((entry) => hasExplicitUsageValue(entry));
}
if (!isRecord(value)) return false;
return Object.values(value).some((entry) => hasExplicitUsageValue(entry));
}
function collectUsageCandidates(payload: unknown): Array<Record<string, unknown>> {
const candidates: Array<Record<string, unknown>> = [];
const visited = new Set<object>();
@@ -253,6 +325,15 @@ export function parseProxyUsage(payload: unknown): ParsedProxyUsage {
return best;
}
export function hasProxyUsagePayload(payload: unknown): boolean {
if (!payload || typeof payload !== 'object') return false;
const candidates = collectUsageCandidates(payload);
return candidates.some((candidate) => (
USAGE_DIRECT_KEYS.some((key) => hasOwn(candidate, key) && hasExplicitUsageValue(candidate[key]))
|| USAGE_DETAIL_KEYS.some((key) => hasOwn(candidate, key) && hasExplicitUsageValue(candidate[key]))
));
}
export function mergeProxyUsage(base: ParsedProxyUsage, incoming: ParsedProxyUsage): ParsedProxyUsage {
const normalizeUsage = (usage: ParsedProxyUsage): ParsedProxyUsage => ({
promptTokens: toPositiveInt(usage.promptTokens),