From a2c97959ba8f2e544356e3ebc7ee86deeecf4ef8 Mon Sep 17 00:00:00 2001 From: Jeff Gu Date: Thu, 9 Apr 2026 11:17:20 +0800 Subject: [PATCH] =?UTF-8?q?feat(channel):=20=E6=94=AF=E6=8C=81=E5=A4=9A?= =?UTF-8?q?=E4=B8=AA=20API=20=E5=AF=86=E9=92=A5=E5=92=8C=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E9=87=8D=E8=AF=95=E5=8A=9F=E8=83=BD=20-=20=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=20ChannelConfig=20=E6=8E=A5=E5=8F=A3=EF=BC=8C=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=20api=5Fkeys=E3=80=81auto=5Fretry=20=E5=92=8C=20auto?= =?UTF-8?q?=5Frotate=20=E5=AD=97=E6=AE=B5=20-=20=E4=BF=AE=E6=94=B9=20Chann?= =?UTF-8?q?els=20=E7=BB=84=E4=BB=B6=E4=BB=A5=E6=94=AF=E6=8C=81=E6=96=B0?= =?UTF-8?q?=E7=9A=84=E9=85=8D=E7=BD=AE=20-=20=E5=AE=9E=E7=8E=B0=20API=20?= =?UTF-8?q?=E8=AF=B7=E6=B1=82=E7=9A=84=E9=87=8D=E8=AF=95=E6=9C=BA=E5=88=B6?= =?UTF-8?q?=EF=BC=8C=E5=A2=9E=E5=BC=BA=E7=A8=B3=E5=AE=9A=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/Channels.tsx | 205 ++++++++++++------ frontend/src/types/index.ts | 7 +- src/admin/channel_api.ts | 18 +- src/channel-config.ts | 70 ++++++ src/providers/azure-openai-proxy.ts | 25 +-- src/providers/azure-openai-responses-proxy.ts | 15 +- src/providers/claude-proxy.ts | 10 +- src/providers/claude-to-openai-proxy.ts | 5 +- src/providers/index.ts | 3 +- src/providers/models.ts | 3 +- src/providers/openai-proxy.ts | 3 +- src/providers/openai-responses-proxy.ts | 3 +- src/providers/shared/azure-target-url.ts | 33 +++ src/providers/shared/channel-resolver.ts | 9 +- src/providers/shared/openai-stream-utils.ts | 2 +- src/providers/shared/provider-registry.ts | 2 +- .../shared/responses-stream-utils.ts | 2 +- src/providers/shared/upstream-retry.ts | 130 +++++++++++ type.d.ts | 6 +- 19 files changed, 432 insertions(+), 119 deletions(-) create mode 100644 src/channel-config.ts create mode 100644 src/providers/shared/azure-target-url.ts create mode 100644 src/providers/shared/upstream-retry.ts diff --git a/frontend/src/pages/Channels.tsx b/frontend/src/pages/Channels.tsx index 242a46d..18eab00 100644 --- a/frontend/src/pages/Channels.tsx +++ b/frontend/src/pages/Channels.tsx @@ -8,6 +8,7 @@ import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Select } from '@/components/ui/select' import { Textarea } from '@/components/ui/textarea' +import { Checkbox } from '@/components/ui/checkbox' import { useToast } from '@/components/ui/use-toast' import { cn } from '@/lib/utils' import { @@ -41,21 +42,61 @@ const channelTypes = [ { value: 'azure-openai-responses', label: 'Azure OpenAI Responses' }, ] +const createDefaultChannelFormData = (): ChannelConfig => ({ + name: '', + type: 'azure-openai', + endpoint: '', + api_keys: [], + auto_retry: true, + auto_rotate: true, + supported_models: [], + deployment_mapper: {}, +}) + +const parseApiKeys = (value: string): string[] => { + const seen = new Set() + + return value + .split('\n') + .map((line) => line.trim()) + .filter((line) => line.length > 0) + .filter((line) => { + if (seen.has(line)) { + return false + } + seen.add(line) + return true + }) +} + +const formatApiKeys = (apiKeys?: string[]): string => { + return (apiKeys || []).join('\n') +} + +const normalizeChannelFormConfig = (config: ChannelConfig): ChannelConfig => { + const legacyApiKey = typeof config.api_key === 'string' ? config.api_key.trim() : '' + const rawApiKeys = Array.isArray(config.api_keys) ? config.api_keys : [] + const mergedKeys = parseApiKeys([legacyApiKey, ...rawApiKeys].join('\n')) + + return { + ...config, + api_key: undefined, + api_keys: mergedKeys, + auto_retry: config.auto_retry ?? true, + auto_rotate: config.auto_rotate ?? true, + supported_models: Array.isArray(config.supported_models) ? config.supported_models : [], + deployment_mapper: config.deployment_mapper || {}, + } +} + export function Channels() { const [view, setView] = useState<'list' | 'form'>('list') const [editMode, setEditMode] = useState('form') const [editingKey, setEditingKey] = useState(null) - const [formData, setFormData] = useState({ - name: '', - type: 'azure-openai', - endpoint: '', - api_key: '', - api_version: '', - supported_models: [], - deployment_mapper: {}, - }) + const [formData, setFormData] = useState(createDefaultChannelFormData()) const [channelKey, setChannelKey] = useState('') const [jsonValue, setJsonValue] = useState('') + const [apiKeysInput, setApiKeysInput] = useState('') const [supportedModelRows, setSupportedModelRows] = useState>([]) const [mapperRows, setMapperRows] = useState>([]) const [searchQuery, setSearchQuery] = useState('') @@ -107,17 +148,10 @@ export function Channels() { }) const resetForm = () => { - setFormData({ - name: '', - type: 'azure-openai', - endpoint: '', - api_key: '', - api_version: '', - supported_models: [], - deployment_mapper: {}, - }) + setFormData(createDefaultChannelFormData()) setChannelKey('') setJsonValue('') + setApiKeysInput('') setSupportedModelRows([]) setMapperRows([]) setEditingKey(null) @@ -132,18 +166,17 @@ export function Channels() { const handleEdit = (channel: Channel) => { setEditingKey(channel.key) setChannelKey(channel.key) - const config = typeof channel.value === 'string' ? JSON.parse(channel.value) : channel.value + const rawConfig = typeof channel.value === 'string' ? JSON.parse(channel.value) : channel.value + const config = normalizeChannelFormConfig(rawConfig) setFormData(config) setJsonValue(JSON.stringify(config, null, 2)) + setApiKeysInput(formatApiKeys(config.api_keys)) setSupportedModelRows((config.supported_models || []).map((model) => ({ model }))) - - if (config.deployment_mapper) { - const rows = Object.entries(config.deployment_mapper).map(([request, deployment]) => ({ - request, - deployment: deployment as string, - })) - setMapperRows(rows) - } + const rows = Object.entries(config.deployment_mapper || {}).map(([request, deployment]) => ({ + request, + deployment: deployment as string, + })) + setMapperRows(rows) setView('form') } @@ -159,13 +192,7 @@ export function Channels() { return } - let config: any - if (editMode === 'form') { - if (!formData.name || !formData.endpoint || !formData.api_key) { - addToast('请填写所有必填字段', 'error') - return - } - + const buildFormConfig = () => { const deployment_mapper: Record = {} mapperRows.forEach((row) => { if (row.request && row.deployment) { @@ -177,18 +204,36 @@ export function Channels() { .map((row) => row.model.trim()) .filter((model) => model.length > 0) - if (supported_models.length === 0) { + return normalizeChannelFormConfig({ + ...formData, + api_key: undefined, + api_keys: parseApiKeys(apiKeysInput), + supported_models, + deployment_mapper, + }) + } + + let config: any + if (editMode === 'form') { + const apiKeys = parseApiKeys(apiKeysInput) + const supportedModels = supportedModelRows + .map((row) => row.model.trim()) + .filter((model) => model.length > 0) + + if (!formData.name || !formData.endpoint || apiKeys.length === 0) { + addToast('请填写所有必填字段', 'error') + return + } + + if (supportedModels.length === 0) { addToast('请至少填写一个支持模型', 'error') return } - config = { ...formData, supported_models, deployment_mapper } - if (!formData.api_version) { - delete config.api_version - } + config = buildFormConfig() } else { try { - config = JSON.parse(jsonValue) + config = normalizeChannelFormConfig(JSON.parse(jsonValue)) } catch { addToast('JSON格式错误', 'error') return @@ -209,21 +254,26 @@ export function Channels() { const supported_models = supportedModelRows .map((row) => row.model.trim()) .filter((model) => model.length > 0) - const config = { ...formData, supported_models, deployment_mapper } + const config = normalizeChannelFormConfig({ + ...formData, + api_key: undefined, + api_keys: parseApiKeys(apiKeysInput), + supported_models, + deployment_mapper, + }) setJsonValue(JSON.stringify(config, null, 2)) setEditMode('json') } else { try { - const config = JSON.parse(jsonValue) + const config = normalizeChannelFormConfig(JSON.parse(jsonValue)) setFormData(config) + setApiKeysInput(formatApiKeys(config.api_keys)) setSupportedModelRows((config.supported_models || []).map((model: string) => ({ model }))) - if (config.deployment_mapper) { - const rows = Object.entries(config.deployment_mapper).map(([request, deployment]) => ({ - request, - deployment: deployment as string, - })) - setMapperRows(rows) - } + const rows = Object.entries(config.deployment_mapper || {}).map(([request, deployment]) => ({ + request, + deployment: deployment as string, + })) + setMapperRows(rows) setEditMode('form') } catch { addToast('JSON格式错误', 'error') @@ -514,26 +564,45 @@ export function Channels() { placeholder="https://your-resource.openai.azure.com/" /> -
-
- - setFormData({ ...formData, api_key: e.target.value })} - placeholder="sk-..." +
+ +