Files
BoosAPI/src/server/routes/proxy/inputFiles.ts
T
cita 7bd3114ea0 feat: implement proxy input file resolution and storage
- Added `inputFiles.ts` for handling input file resolution, including validation and normalization of file types.
- Introduced `proxyFileStore.ts` for managing file storage, retrieval, and deletion with owner-based isolation.
- Created tests for proxy file storage functionality in `proxyFileStore.test.ts`.
- Developed `proxyInputFileResolver.ts` to resolve input files in OpenAI and response formats.
- Added utility functions for handling reasoning signatures in `reasoningTransport.ts` and corresponding tests.
- Enhanced shared input file transformers in `inputFile.ts` for better file handling and type inference.
2026-03-08 22:10:31 +08:00

115 lines
3.5 KiB
TypeScript

import type { ProxyResourceOwner } from '../../middleware/auth.js';
import { getProxyFileByPublicIdForOwner } from '../../services/proxyFileStore.js';
import {
inferInputFileMimeType,
normalizeInputFileBlock,
toOpenAiChatFileBlock,
toResponsesInputFileBlock,
type NormalizedInputFile,
} from '../../transformers/shared/inputFile.js';
function isRecord(value: unknown): value is Record<string, unknown> {
return !!value && typeof value === 'object';
}
function isSupportedFileMimeType(mimeType: string): boolean {
return mimeType === 'application/pdf'
|| mimeType === 'text/plain'
|| mimeType === 'text/markdown'
|| mimeType === 'application/json'
|| mimeType.startsWith('image/')
|| mimeType.startsWith('audio/');
}
export class ProxyInputFileResolutionError extends Error {
statusCode: number;
payload: unknown;
constructor(statusCode: number, payload: unknown) {
super(typeof payload === 'object' && payload && 'error' in (payload as Record<string, unknown>)
? String(((payload as { error?: { message?: unknown } }).error?.message) || 'input file resolution failed')
: 'input file resolution failed');
this.name = 'ProxyInputFileResolutionError';
this.statusCode = statusCode;
this.payload = payload;
}
}
function buildInvalidRequest(message: string): ProxyInputFileResolutionError {
return new ProxyInputFileResolutionError(400, {
error: {
message,
type: 'invalid_request_error',
},
});
}
function buildNotFound(message: string): ProxyInputFileResolutionError {
return new ProxyInputFileResolutionError(404, {
error: {
message,
type: 'not_found_error',
},
});
}
async function resolveInputFileBlock(
item: Record<string, unknown>,
owner: ProxyResourceOwner,
): Promise<Record<string, unknown>> {
const normalized = normalizeInputFileBlock(item);
if (!normalized) return item;
let resolved: NormalizedInputFile = { ...normalized };
if (!resolved.fileData && resolved.fileId?.startsWith('file-metapi-')) {
const stored = await getProxyFileByPublicIdForOwner(resolved.fileId, owner);
if (!stored) {
throw buildNotFound(`file not found: ${resolved.fileId}`);
}
resolved = {
filename: stored.filename,
mimeType: stored.mimeType,
fileData: stored.contentBase64,
};
}
const inferredMimeType = inferInputFileMimeType(resolved);
if (resolved.fileData && (!inferredMimeType || !isSupportedFileMimeType(inferredMimeType))) {
throw buildInvalidRequest(`unsupported file mime type: ${inferredMimeType || 'application/octet-stream'}`);
}
if (inferredMimeType) {
resolved.mimeType = inferredMimeType;
}
const type = typeof item.type === 'string' ? item.type : '';
if (type === 'input_file') {
return toResponsesInputFileBlock(resolved);
}
if (type === 'file') {
return toOpenAiChatFileBlock(resolved);
}
return item;
}
export async function inlineLocalInputFileReferences(
value: unknown,
owner: ProxyResourceOwner,
): Promise<unknown> {
if (Array.isArray(value)) {
return Promise.all(value.map((item) => inlineLocalInputFileReferences(item, owner)));
}
if (!isRecord(value)) return value;
const type = typeof value.type === 'string' ? value.type : '';
if (type === 'input_file' || type === 'file') {
return resolveInputFileBlock(value, owner);
}
const entries = await Promise.all(
Object.entries(value).map(async ([key, entryValue]) => [key, await inlineLocalInputFileReferences(entryValue, owner)] as const),
);
return Object.fromEntries(entries);
}