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.
This commit is contained in:
cita
2026-03-08 22:10:31 +08:00
parent ede8998244
commit 7bd3114ea0
54 changed files with 4504 additions and 164 deletions
@@ -523,6 +523,8 @@ describe('Gemini aggregate/outbound/usage', () => {
completionTokens: 11,
totalTokens: 31,
cachedTokens: 4,
cacheReadTokens: 4,
cacheCreationTokens: 0,
reasoningTokens: 4,
});
expect(extractGeminiUsage(serialized)).toEqual({
@@ -530,6 +532,8 @@ describe('Gemini aggregate/outbound/usage', () => {
completionTokens: 11,
totalTokens: 31,
cachedTokens: 4,
cacheReadTokens: 4,
cacheCreationTokens: 0,
reasoningTokens: 4,
});
});
@@ -550,6 +554,8 @@ describe('Gemini aggregate/outbound/usage', () => {
completionTokens: 6,
totalTokens: 6,
cachedTokens: 0,
cacheReadTokens: 0,
cacheCreationTokens: 0,
reasoningTokens: 0,
});
});
@@ -570,6 +570,8 @@ describe('extractGeminiUsage', () => {
completionTokens: 10,
totalTokens: 18,
cachedTokens: 5,
cacheReadTokens: 5,
cacheCreationTokens: 0,
reasoningTokens: 3,
});
});
@@ -6,6 +6,10 @@ function baseIncludesVersion(baseUrl: string): boolean {
return /\/v\d+(?:beta)?(?:\/|$)/i.test(baseUrl);
}
function asTrimmedString(value: unknown): string {
return typeof value === 'string' ? value.trim() : '';
}
export function resolveGeminiNativeBaseUrl(baseUrl: string, apiVersion: string): string {
const normalized = normalizeBaseUrl(baseUrl);
if (baseIncludesVersion(normalized)) return normalized;
@@ -37,6 +41,42 @@ export function resolveGeminiGenerateContentUrl(
return `${base}/${normalizedAction}${query ? `?${query}` : ''}`;
}
export function resolveGeminiProxyApiVersion(params: { geminiApiVersion?: unknown } | null | undefined): string {
return asTrimmedString(params?.geminiApiVersion) || 'v1beta';
}
export function parseGeminiProxyRequestPath(input: {
rawUrl?: string | null;
params?: { geminiApiVersion?: unknown } | null;
}): {
apiVersion: string;
modelActionPath: string;
requestedModel: string;
isStreamAction: boolean;
} {
const apiVersion = resolveGeminiProxyApiVersion(input.params);
const rawUrl = asTrimmedString(input.rawUrl);
const withoutQuery = rawUrl.split('?')[0] || rawUrl;
const normalizedVersion = apiVersion.replace(/^\/+/, '');
const geminiPrefix = `/gemini/${normalizedVersion}/`;
const aliasPrefix = `/${normalizedVersion}/`;
let modelActionPath = withoutQuery.replace(/^\/+/, '');
if (withoutQuery.startsWith(geminiPrefix)) {
modelActionPath = withoutQuery.slice(geminiPrefix.length);
} else if (withoutQuery.startsWith(aliasPrefix)) {
modelActionPath = withoutQuery.slice(aliasPrefix.length);
}
const requestedModel = modelActionPath.replace(/^models\//, '').split(':')[0].trim();
return {
apiVersion,
modelActionPath,
requestedModel,
isStreamAction: modelActionPath.endsWith(':streamGenerateContent'),
};
}
import { geminiGenerateContentInbound } from './inbound.js';
import { geminiGenerateContentOutbound } from './outbound.js';
import { geminiGenerateContentStream } from './stream.js';
@@ -58,6 +98,8 @@ export const geminiGenerateContentTransformer = {
reasoningEffortToGeminiThinkingConfig,
geminiThinkingConfigToReasoning,
},
parseProxyRequestPath: parseGeminiProxyRequestPath,
resolveProxyApiVersion: resolveGeminiProxyApiVersion,
resolveBaseUrl: resolveGeminiNativeBaseUrl,
resolveModelsUrl: resolveGeminiModelsUrl,
resolveActionUrl: resolveGeminiGenerateContentUrl,
@@ -37,6 +37,8 @@ export function extractGeminiUsage(payload: unknown): NormalizedUsage {
completionTokens,
totalTokens,
cachedTokens,
cacheReadTokens: cachedTokens,
cacheCreationTokens: 0,
reasoningTokens,
};
}