From 1a90c280e0e00dcbd54f1627805c03812de32af4 Mon Sep 17 00:00:00 2001 From: Dream Hunter Date: Sun, 15 Mar 2026 18:32:50 +0800 Subject: [PATCH] Add dedicated audio channel support (#11) --- frontend/src/api/client.ts | 9 ++ frontend/src/pages/ApiTest.tsx | 6 ++ frontend/src/pages/Channels.tsx | 95 +++++++++++++++++++-- frontend/src/pages/Pricing.tsx | 25 ++++-- frontend/src/types/index.ts | 6 +- src/admin/channel_api.ts | 3 + src/admin/pricing_api.ts | 4 + src/admin/token_utils.ts | 13 +-- src/constants.ts | 2 +- src/db/index.ts | 26 +++++- src/providers/azure-openai-proxy.ts | 2 +- src/providers/index.ts | 1 + src/providers/models.ts | 7 +- src/providers/openai-proxy.ts | 2 +- src/providers/shared/channel-resolver.ts | 16 +++- src/providers/shared/openai-stream-utils.ts | 73 +++++++++++++++- src/providers/shared/provider-registry.ts | 2 + src/providers/shared/route-policy.ts | 15 +++- src/utils.ts | 29 +++++++ type.d.ts | 4 + 20 files changed, 303 insertions(+), 37 deletions(-) diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index fde02bc..501cb24 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -103,6 +103,15 @@ async function responseInterceptor(response: Response): Promise { return response.json() } + if (contentType && contentType.startsWith('audio/')) { + const blob = await response.blob() + return { + object: 'audio', + contentType, + size: blob.size, + } + } + return response.text() } diff --git a/frontend/src/pages/ApiTest.tsx b/frontend/src/pages/ApiTest.tsx index 401dbd3..a57ce72 100644 --- a/frontend/src/pages/ApiTest.tsx +++ b/frontend/src/pages/ApiTest.tsx @@ -38,6 +38,11 @@ const requestTemplates: Record = { model: 'gpt-4o-mini', input: 'Hello, Responses API!', }, + '/v1/audio/speech': { + model: 'gpt-4o-mini-tts', + input: 'The quick brown fox jumped over the lazy dog', + voice: 'alloy', + }, '/v1/completions': { model: 'gpt-3.5-turbo-instruct', prompt: 'Once upon a time', @@ -137,6 +142,7 @@ export function ApiTest() { + diff --git a/frontend/src/pages/Channels.tsx b/frontend/src/pages/Channels.tsx index 342f3bb..51343f6 100644 --- a/frontend/src/pages/Channels.tsx +++ b/frontend/src/pages/Channels.tsx @@ -33,6 +33,8 @@ type EditMode = 'form' | 'json' const channelTypes = [ { value: 'azure-openai', label: 'Azure OpenAI' }, { value: 'openai', label: 'OpenAI' }, + { value: 'azure-openai-audio', label: 'Azure OpenAI Audio' }, + { value: 'openai-audio', label: 'OpenAI Audio' }, { value: 'claude', label: 'Claude' }, { value: 'claude-to-openai', label: 'Claude → OpenAI' }, { value: 'openai-responses', label: 'OpenAI Responses' }, @@ -49,10 +51,12 @@ export function Channels() { endpoint: '', api_key: '', api_version: '', + supported_models: [], deployment_mapper: {}, }) const [channelKey, setChannelKey] = useState('') const [jsonValue, setJsonValue] = useState('') + const [supportedModelRows, setSupportedModelRows] = useState>([]) const [mapperRows, setMapperRows] = useState>([]) const [searchQuery, setSearchQuery] = useState('') const [openMenu, setOpenMenu] = useState(null) @@ -109,10 +113,12 @@ export function Channels() { endpoint: '', api_key: '', api_version: '', + supported_models: [], deployment_mapper: {}, }) setChannelKey('') setJsonValue('') + setSupportedModelRows([]) setMapperRows([]) setEditingKey(null) setEditMode('form') @@ -129,6 +135,7 @@ export function Channels() { const config = typeof channel.value === 'string' ? JSON.parse(channel.value) : channel.value setFormData(config) setJsonValue(JSON.stringify(config, null, 2)) + setSupportedModelRows((config.supported_models || []).map((model) => ({ model }))) if (config.deployment_mapper) { const rows = Object.entries(config.deployment_mapper).map(([request, deployment]) => ({ @@ -166,7 +173,16 @@ export function Channels() { } }) - config = { ...formData, deployment_mapper } + const supported_models = supportedModelRows + .map((row) => row.model.trim()) + .filter((model) => model.length > 0) + + if (supported_models.length === 0) { + addToast('请至少填写一个支持模型', 'error') + return + } + + config = { ...formData, supported_models, deployment_mapper } if (!formData.api_version) { delete config.api_version } @@ -190,13 +206,17 @@ export function Channels() { deployment_mapper[row.request] = row.deployment } }) - const config = { ...formData, deployment_mapper } + const supported_models = supportedModelRows + .map((row) => row.model.trim()) + .filter((model) => model.length > 0) + const config = { ...formData, supported_models, deployment_mapper } setJsonValue(JSON.stringify(config, null, 2)) setEditMode('json') } else { try { const config = JSON.parse(jsonValue) setFormData(config) + setSupportedModelRows((config.supported_models || []).map((model: string) => ({ model }))) if (config.deployment_mapper) { const rows = Object.entries(config.deployment_mapper).map(([request, deployment]) => ({ request, @@ -215,6 +235,20 @@ export function Channels() { setMapperRows([...mapperRows, { request: '', deployment: '' }]) } + const addSupportedModelRow = () => { + setSupportedModelRows([...supportedModelRows, { model: '' }]) + } + + const removeSupportedModelRow = (index: number) => { + setSupportedModelRows(supportedModelRows.filter((_, i) => i !== index)) + } + + const updateSupportedModelRow = (index: number, value: string) => { + const newRows = [...supportedModelRows] + newRows[index].model = value + setSupportedModelRows(newRows) + } + const removeMapperRow = (index: number) => { setMapperRows(mapperRows.filter((_, i) => i !== index)) } @@ -299,7 +333,7 @@ export function Channels() {
{filteredData?.map((channel) => { const config = typeof channel.value === 'string' ? JSON.parse(channel.value) : channel.value - const modelCount = Object.keys(config.deployment_mapper || {}).length + const modelCount = (config.supported_models || []).length const isMenuOpen = openMenu === channel.key return ( @@ -490,7 +524,7 @@ export function Channels() { placeholder="sk-..." />
- {(formData.type === 'azure-openai' || formData.type === 'claude' || formData.type === 'azure-openai-responses') && ( + {(formData.type === 'azure-openai' || formData.type === 'azure-openai-audio' || formData.type === 'claude' || formData.type === 'azure-openai-responses') && (
+ + +
+
+

+ + 支持模型 +

+

用于声明这个频道接受哪些请求模型名,可填写精确值或通配符

+
+ +
+ + {supportedModelRows.length === 0 ? ( + + ) : ( +
+ {supportedModelRows.map((row, index) => ( +
+ updateSupportedModelRow(index, e.target.value)} + placeholder="gpt-4o-mini-tts 或 gpt-*" + className="flex-1 bg-background text-sm" + /> + +
+ ))} +
+ )} +
+
+ {/* Model Mappings */} @@ -514,7 +599,7 @@ export function Channels() { 模型映射 -

将请求模型名映射到实际部署名

+

可选。将请求模型名映射到实际上游 deployment;未填写时默认使用同名