* fix: normalize cross-database json boundaries * fix: address json boundary review comments --------- Co-authored-by: apple <apple@appledeMacBook-Pro.local>
283 lines
8.9 KiB
TypeScript
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;
|
|
}
|
|
}
|
|
}
|