refactor: share platform file and route helpers
This commit is contained in:
+16
@@ -0,0 +1,16 @@
|
||||
export type ConversationFileKind = 'document' | 'image' | 'audio' | 'unknown';
|
||||
|
||||
export declare const CONVERSATION_DOCUMENT_ACCEPT_PARTS: string[];
|
||||
export declare function classifyConversationFileMimeType(
|
||||
mimeType: string | null | undefined,
|
||||
): Exclude<ConversationFileKind, 'unknown'>;
|
||||
export declare function detectConversationFileKind(file: {
|
||||
filename?: string | null;
|
||||
mimeType?: string | null;
|
||||
}): ConversationFileKind;
|
||||
export declare function isSupportedConversationFileMimeType(mimeType: string): boolean;
|
||||
export declare function buildConversationAcceptList(input: {
|
||||
document: boolean;
|
||||
image: boolean;
|
||||
audio: boolean;
|
||||
}): string;
|
||||
@@ -0,0 +1,52 @@
|
||||
export const CONVERSATION_DOCUMENT_ACCEPT_PARTS = ['.pdf', '.txt', '.md', '.markdown', '.json'];
|
||||
const DOCUMENT_MIME_TYPES = new Set([
|
||||
'application/json',
|
||||
'application/pdf',
|
||||
'text/markdown',
|
||||
'text/plain',
|
||||
]);
|
||||
const DOCUMENT_EXTENSIONS = ['.json', '.md', '.markdown', '.pdf', '.txt'];
|
||||
const IMAGE_EXTENSIONS = ['.avif', '.bmp', '.gif', '.jpeg', '.jpg', '.png', '.svg', '.webp'];
|
||||
const AUDIO_EXTENSIONS = ['.aac', '.flac', '.m4a', '.mp3', '.ogg', '.wav', '.weba'];
|
||||
|
||||
function normalizeValue(value) {
|
||||
return (value || '').trim().toLowerCase();
|
||||
}
|
||||
|
||||
export function classifyConversationFileMimeType(mimeType) {
|
||||
const normalized = normalizeValue(mimeType);
|
||||
if (normalized.startsWith('image/')) return 'image';
|
||||
if (normalized.startsWith('audio/')) return 'audio';
|
||||
return 'document';
|
||||
}
|
||||
|
||||
export function detectConversationFileKind(file) {
|
||||
const mimeType = normalizeValue(file?.mimeType);
|
||||
if (mimeType) {
|
||||
if (mimeType.startsWith('image/')) return 'image';
|
||||
if (mimeType.startsWith('audio/')) return 'audio';
|
||||
if (DOCUMENT_MIME_TYPES.has(mimeType)) return 'document';
|
||||
}
|
||||
|
||||
const filename = normalizeValue(file?.filename);
|
||||
if (!filename) return 'unknown';
|
||||
if (DOCUMENT_EXTENSIONS.some((extension) => filename.endsWith(extension))) return 'document';
|
||||
if (IMAGE_EXTENSIONS.some((extension) => filename.endsWith(extension))) return 'image';
|
||||
if (AUDIO_EXTENSIONS.some((extension) => filename.endsWith(extension))) return 'audio';
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
export function isSupportedConversationFileMimeType(mimeType) {
|
||||
const normalized = normalizeValue(mimeType);
|
||||
return DOCUMENT_MIME_TYPES.has(normalized)
|
||||
|| normalized.startsWith('image/')
|
||||
|| normalized.startsWith('audio/');
|
||||
}
|
||||
|
||||
export function buildConversationAcceptList(input) {
|
||||
const parts = [];
|
||||
if (input.document) parts.push(...CONVERSATION_DOCUMENT_ACCEPT_PARTS);
|
||||
if (input.image) parts.push('image/*');
|
||||
if (input.audio) parts.push('audio/*');
|
||||
return parts.join(',');
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
buildConversationAcceptList,
|
||||
classifyConversationFileMimeType,
|
||||
detectConversationFileKind,
|
||||
isSupportedConversationFileMimeType,
|
||||
} from './conversationFileTypes.js';
|
||||
|
||||
describe('conversationFileTypes', () => {
|
||||
it('classifies conversation file kinds from mime type and filename', () => {
|
||||
expect(classifyConversationFileMimeType('image/png')).toBe('image');
|
||||
expect(classifyConversationFileMimeType('audio/mpeg')).toBe('audio');
|
||||
expect(classifyConversationFileMimeType('application/pdf')).toBe('document');
|
||||
|
||||
expect(detectConversationFileKind({ filename: 'paper.pdf', mimeType: null })).toBe('document');
|
||||
expect(detectConversationFileKind({ filename: 'photo.webp', mimeType: null })).toBe('image');
|
||||
expect(detectConversationFileKind({ filename: 'voice.mp3', mimeType: null })).toBe('audio');
|
||||
expect(detectConversationFileKind({ filename: 'unknown.bin', mimeType: null })).toBe('unknown');
|
||||
});
|
||||
|
||||
it('keeps supported mime and accept list rules in one place', () => {
|
||||
expect(isSupportedConversationFileMimeType('application/pdf')).toBe(true);
|
||||
expect(isSupportedConversationFileMimeType('image/jpeg')).toBe(true);
|
||||
expect(isSupportedConversationFileMimeType('audio/wav')).toBe(true);
|
||||
expect(isSupportedConversationFileMimeType('application/octet-stream')).toBe(false);
|
||||
|
||||
expect(buildConversationAcceptList({
|
||||
document: true,
|
||||
image: true,
|
||||
audio: false,
|
||||
})).toBe('.pdf,.txt,.md,.markdown,.json,image/*');
|
||||
});
|
||||
});
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
export declare const PLATFORM_ALIASES: Record<string, string>;
|
||||
export declare function normalizePlatformAlias(platform: unknown): string;
|
||||
export declare function detectPlatformByUrlHint(url: string): string | undefined;
|
||||
@@ -0,0 +1,65 @@
|
||||
export const PLATFORM_ALIASES = {
|
||||
anyrouter: 'anyrouter',
|
||||
'wong-gongyi': 'new-api',
|
||||
'vo-api': 'new-api',
|
||||
'super-api': 'new-api',
|
||||
'rix-api': 'new-api',
|
||||
'neo-api': 'new-api',
|
||||
newapi: 'new-api',
|
||||
'new api': 'new-api',
|
||||
'new-api': 'new-api',
|
||||
oneapi: 'one-api',
|
||||
'one api': 'one-api',
|
||||
'one-api': 'one-api',
|
||||
onehub: 'one-hub',
|
||||
'one-hub': 'one-hub',
|
||||
donehub: 'done-hub',
|
||||
'done-hub': 'done-hub',
|
||||
veloera: 'veloera',
|
||||
sub2api: 'sub2api',
|
||||
openai: 'openai',
|
||||
codex: 'codex',
|
||||
'chatgpt-codex': 'codex',
|
||||
'chatgpt codex': 'codex',
|
||||
anthropic: 'claude',
|
||||
claude: 'claude',
|
||||
gemini: 'gemini',
|
||||
'gemini-cli': 'gemini-cli',
|
||||
antigravity: 'antigravity',
|
||||
'anti-gravity': 'antigravity',
|
||||
google: 'gemini',
|
||||
cliproxyapi: 'cliproxyapi',
|
||||
cpa: 'cliproxyapi',
|
||||
'cli-proxy-api': 'cliproxyapi',
|
||||
};
|
||||
|
||||
export function normalizePlatformAlias(platform) {
|
||||
const raw = typeof platform === 'string' ? platform.trim().toLowerCase() : '';
|
||||
if (!raw) return '';
|
||||
return PLATFORM_ALIASES[raw] ?? raw;
|
||||
}
|
||||
|
||||
export function detectPlatformByUrlHint(url) {
|
||||
const normalized = (url || '').trim().toLowerCase();
|
||||
if (!normalized) return undefined;
|
||||
|
||||
if (normalized.includes('api.openai.com')) return 'openai';
|
||||
if (normalized.includes('chatgpt.com/backend-api/codex')) return 'codex';
|
||||
if (normalized.includes('api.anthropic.com') || normalized.includes('anthropic.com/v1')) return 'claude';
|
||||
if (
|
||||
normalized.includes('generativelanguage.googleapis.com')
|
||||
|| normalized.includes('googleapis.com/v1beta/openai')
|
||||
|| normalized.includes('gemini.google.com')
|
||||
) {
|
||||
return 'gemini';
|
||||
}
|
||||
if (normalized.includes('cloudcode-pa.googleapis.com')) return 'gemini-cli';
|
||||
if (normalized.includes('anyrouter')) return 'anyrouter';
|
||||
if (normalized.includes('donehub') || normalized.includes('done-hub')) return 'done-hub';
|
||||
if (normalized.includes('onehub') || normalized.includes('one-hub')) return 'one-hub';
|
||||
if (normalized.includes('veloera')) return 'veloera';
|
||||
if (normalized.includes('sub2api')) return 'sub2api';
|
||||
if (normalized.includes('127.0.0.1:8317') || normalized.includes('localhost:8317')) return 'cliproxyapi';
|
||||
|
||||
return undefined;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
detectPlatformByUrlHint,
|
||||
normalizePlatformAlias,
|
||||
} from './platformIdentity.js';
|
||||
|
||||
describe('platformIdentity', () => {
|
||||
it('normalizes shared platform aliases', () => {
|
||||
expect(normalizePlatformAlias('chatgpt-codex')).toBe('codex');
|
||||
expect(normalizePlatformAlias('anti-gravity')).toBe('antigravity');
|
||||
expect(normalizePlatformAlias('one api')).toBe('one-api');
|
||||
expect(normalizePlatformAlias('')).toBe('');
|
||||
});
|
||||
|
||||
it('detects platform by well-known url hints', () => {
|
||||
expect(detectPlatformByUrlHint('https://api.openai.com/v1/models')).toBe('openai');
|
||||
expect(detectPlatformByUrlHint('https://chatgpt.com/backend-api/codex')).toBe('codex');
|
||||
expect(detectPlatformByUrlHint('https://api.anthropic.com/v1/messages')).toBe('claude');
|
||||
expect(detectPlatformByUrlHint('https://generativelanguage.googleapis.com/v1beta')).toBe('gemini');
|
||||
expect(detectPlatformByUrlHint('https://cloudcode-pa.googleapis.com')).toBe('gemini-cli');
|
||||
expect(detectPlatformByUrlHint('http://127.0.0.1:8317/v1/models')).toBe('cliproxyapi');
|
||||
});
|
||||
});
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
export type ParsedProxyLogMetadata = {
|
||||
clientKind: string | null;
|
||||
sessionId: string | null;
|
||||
downstreamPath: string | null;
|
||||
upstreamPath: string | null;
|
||||
messageText: string;
|
||||
};
|
||||
|
||||
export declare function parseProxyLogMetadata(rawMessage: string): ParsedProxyLogMetadata;
|
||||
@@ -0,0 +1,18 @@
|
||||
export function parseProxyLogMetadata(rawMessage) {
|
||||
const clientMatch = rawMessage.match(/\[client:([^\]]+)\]/i);
|
||||
const sessionMatch = rawMessage.match(/\[session:([^\]]+)\]/i);
|
||||
const downstreamMatch = rawMessage.match(/\[downstream:([^\]]+)\]/i);
|
||||
const upstreamMatch = rawMessage.match(/\[upstream:([^\]]+)\]/i);
|
||||
const messageText = rawMessage.replace(
|
||||
/^\s*(?:\[(?:client|session|downstream|upstream):[^\]]+\]\s*)+/i,
|
||||
'',
|
||||
).trim();
|
||||
|
||||
return {
|
||||
clientKind: clientMatch?.[1]?.trim() || null,
|
||||
sessionId: sessionMatch?.[1]?.trim() || null,
|
||||
downstreamPath: downstreamMatch?.[1]?.trim() || null,
|
||||
upstreamPath: upstreamMatch?.[1]?.trim() || null,
|
||||
messageText,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { parseProxyLogMetadata } from './proxyLogMeta.js';
|
||||
|
||||
describe('proxyLogMeta', () => {
|
||||
it('parses all supported proxy log prefixes', () => {
|
||||
expect(parseProxyLogMetadata(
|
||||
'[client:codex] [session:turn-1] [downstream:/v1/responses] [upstream:/responses] boom',
|
||||
)).toEqual({
|
||||
clientKind: 'codex',
|
||||
sessionId: 'turn-1',
|
||||
downstreamPath: '/v1/responses',
|
||||
upstreamPath: '/responses',
|
||||
messageText: 'boom',
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps plain message text when no metadata prefixes exist', () => {
|
||||
expect(parseProxyLogMetadata('network timeout')).toEqual({
|
||||
clientKind: null,
|
||||
sessionId: null,
|
||||
downstreamPath: null,
|
||||
upstreamPath: null,
|
||||
messageText: 'network timeout',
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user