Files
metapi/src/server/services/proxyLogStore.ts
T
Citaandapple b3387dfba7 [codex] fix cross-database JSON boundary handling (#283)
* fix: normalize cross-database json boundaries

* fix: address json boundary review comments

---------

Co-authored-by: apple <apple@appledeMacBook-Pro.local>
2026-03-26 20:21:19 +08:00

283 lines
8.9 KiB
TypeScript

import {
db,
schema,
hasProxyLogBillingDetailsColumn,
hasProxyLogClientColumns,
hasProxyLogDownstreamApiKeyIdColumn,
} from '../db/index.js';
export type ProxyLogInsertInput = {
routeId?: number | null;
channelId?: number | null;
accountId?: number | null;
downstreamApiKeyId?: number | null;
modelRequested?: string | null;
modelActual?: string | null;
status?: string | null;
httpStatus?: number | null;
latencyMs?: number | null;
promptTokens?: number | null;
completionTokens?: number | null;
totalTokens?: number | null;
estimatedCost?: number | null;
billingDetails?: unknown;
clientFamily?: string | null;
clientAppId?: string | null;
clientAppName?: string | null;
clientConfidence?: string | null;
errorMessage?: string | null;
retryCount?: number | null;
createdAt?: string | null;
};
function buildProxyLogCoreSelectFields() {
return {
id: schema.proxyLogs.id,
routeId: schema.proxyLogs.routeId,
channelId: schema.proxyLogs.channelId,
accountId: schema.proxyLogs.accountId,
downstreamApiKeyId: schema.proxyLogs.downstreamApiKeyId,
modelRequested: schema.proxyLogs.modelRequested,
modelActual: schema.proxyLogs.modelActual,
status: schema.proxyLogs.status,
httpStatus: schema.proxyLogs.httpStatus,
latencyMs: schema.proxyLogs.latencyMs,
promptTokens: schema.proxyLogs.promptTokens,
completionTokens: schema.proxyLogs.completionTokens,
totalTokens: schema.proxyLogs.totalTokens,
estimatedCost: schema.proxyLogs.estimatedCost,
errorMessage: schema.proxyLogs.errorMessage,
retryCount: schema.proxyLogs.retryCount,
createdAt: schema.proxyLogs.createdAt,
};
}
function buildProxyLogClientSelectFields() {
return {
clientFamily: schema.proxyLogs.clientFamily,
clientAppId: schema.proxyLogs.clientAppId,
clientAppName: schema.proxyLogs.clientAppName,
clientConfidence: schema.proxyLogs.clientConfidence,
};
}
function buildProxyLogSelectFields(options?: {
includeBillingDetails?: boolean;
includeClientFields?: boolean;
}) {
return {
...buildProxyLogCoreSelectFields(),
...(options?.includeClientFields ? buildProxyLogClientSelectFields() : {}),
...(options?.includeBillingDetails ? { billingDetails: schema.proxyLogs.billingDetails } : {}),
};
}
export function getProxyLogBaseSelectFields() {
return buildProxyLogCoreSelectFields();
}
export type ProxyLogSelectFields = ReturnType<typeof buildProxyLogSelectFields>;
export type ResolvedProxyLogSelectFields = {
includeBillingDetails: boolean;
includeClientFields: boolean;
fields: ProxyLogSelectFields;
};
export async function resolveProxyLogSelectFields(options?: {
includeBillingDetails?: boolean;
includeClientFields?: boolean;
}) {
const includeBillingDetails = options?.includeBillingDetails === true
&& await hasProxyLogBillingDetailsColumn();
const includeClientFields = options?.includeClientFields !== false
&& await hasProxyLogClientColumns();
return {
includeBillingDetails,
includeClientFields,
fields: buildProxyLogSelectFields({
includeBillingDetails,
includeClientFields,
}),
};
}
export async function withProxyLogSelectFields<T>(
runner: (selection: ResolvedProxyLogSelectFields) => Promise<T>,
options?: { includeBillingDetails?: boolean; includeClientFields?: boolean },
): Promise<T> {
let selection = await resolveProxyLogSelectFields(options);
while (true) {
try {
return await runner(selection);
} catch (error) {
if (selection.includeBillingDetails && isMissingBillingDetailsColumnError(error)) {
selection = {
includeBillingDetails: false,
includeClientFields: selection.includeClientFields,
fields: buildProxyLogSelectFields({
includeBillingDetails: false,
includeClientFields: selection.includeClientFields,
}),
};
continue;
}
if (selection.includeClientFields && isMissingProxyLogClientColumnsError(error)) {
selection = {
includeBillingDetails: selection.includeBillingDetails,
includeClientFields: false,
fields: buildProxyLogSelectFields({
includeBillingDetails: selection.includeBillingDetails,
includeClientFields: false,
}),
};
continue;
}
throw error;
}
}
}
export function parseProxyLogBillingDetails(value: unknown): Record<string, unknown> | null {
if (value && typeof value === 'object' && !Array.isArray(value)) {
return value as Record<string, unknown>;
}
if (typeof value !== 'string' || value.trim().length === 0) return null;
try {
const parsed = JSON.parse(value);
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
? parsed as Record<string, unknown>
: null;
} catch {
return null;
}
}
function normalizeProxyLogStoreErrorMessage(error: unknown): string {
const message = typeof error === 'object' && error && 'message' in error
? String((error as { message?: unknown }).message || '')
: String(error || '');
return message.toLowerCase();
}
export function isMissingBillingDetailsColumnError(error: unknown): boolean {
const lowered = normalizeProxyLogStoreErrorMessage(error);
return lowered.includes('billing_details')
&& (
lowered.includes('does not exist')
|| lowered.includes('unknown column')
|| lowered.includes('no such column')
|| lowered.includes('has no column named')
);
}
export function isMissingDownstreamApiKeyIdColumnError(error: unknown): boolean {
const lowered = normalizeProxyLogStoreErrorMessage(error);
return lowered.includes('downstream_api_key_id')
&& (
lowered.includes('does not exist')
|| lowered.includes('unknown column')
|| lowered.includes('no such column')
|| lowered.includes('has no column named')
);
}
export function isMissingProxyLogClientColumnsError(error: unknown): boolean {
const lowered = normalizeProxyLogStoreErrorMessage(error);
const hasClientColumnReference = [
'client_family',
'client_app_id',
'client_app_name',
'client_confidence',
].some((columnName) => lowered.includes(columnName));
return hasClientColumnReference
&& (
lowered.includes('does not exist')
|| lowered.includes('unknown column')
|| lowered.includes('no such column')
|| lowered.includes('has no column named')
);
}
export async function insertProxyLog(input: ProxyLogInsertInput): Promise<void> {
const baseValues = {
routeId: input.routeId ?? null,
channelId: input.channelId ?? null,
accountId: input.accountId ?? null,
modelRequested: input.modelRequested ?? null,
modelActual: input.modelActual ?? null,
status: input.status ?? null,
httpStatus: input.httpStatus ?? null,
latencyMs: input.latencyMs ?? null,
promptTokens: input.promptTokens ?? 0,
completionTokens: input.completionTokens ?? 0,
totalTokens: input.totalTokens ?? 0,
estimatedCost: input.estimatedCost ?? 0,
errorMessage: input.errorMessage ?? null,
retryCount: input.retryCount ?? 0,
createdAt: input.createdAt ?? null,
};
const serializedBillingDetails = input.billingDetails == null
? null
: JSON.stringify(input.billingDetails);
const includeBillingDetails = serializedBillingDetails !== null
&& await hasProxyLogBillingDetailsColumn();
const includeDownstreamApiKeyId = input.downstreamApiKeyId != null
&& await hasProxyLogDownstreamApiKeyIdColumn();
const requestedClientFields = [
input.clientFamily,
input.clientAppId,
input.clientAppName,
input.clientConfidence,
].some((value) => value != null && String(value).trim().length > 0);
const includeClientFields = requestedClientFields
&& await hasProxyLogClientColumns();
let allowBillingDetails = includeBillingDetails;
let allowDownstreamApiKeyId = includeDownstreamApiKeyId;
let allowClientFields = includeClientFields;
while (true) {
const values = {
...baseValues,
...(allowBillingDetails ? { billingDetails: serializedBillingDetails } : {}),
...(allowDownstreamApiKeyId ? { downstreamApiKeyId: input.downstreamApiKeyId } : {}),
...(allowClientFields
? {
clientFamily: input.clientFamily ?? null,
clientAppId: input.clientAppId ?? null,
clientAppName: input.clientAppName ?? null,
clientConfidence: input.clientConfidence ?? null,
}
: {}),
};
try {
await db.insert(schema.proxyLogs).values(values).run();
return;
} catch (error) {
if (allowBillingDetails && isMissingBillingDetailsColumnError(error)) {
allowBillingDetails = false;
continue;
}
if (allowDownstreamApiKeyId && isMissingDownstreamApiKeyIdColumnError(error)) {
allowDownstreamApiKeyId = false;
continue;
}
if (allowClientFields && isMissingProxyLogClientColumnsError(error)) {
allowClientFields = false;
continue;
}
throw error;
}
}
}