Skip to content

Commit 51fd63f

Browse files
authored
✨ Model access supports modification access - 2. Add batch modification functionality
✨ Model access supports modification access - 2. Add batch modification functionality
2 parents 38d6fc7 + 4fb5a75 commit 51fd63f

File tree

8 files changed

+500
-45
lines changed

8 files changed

+500
-45
lines changed

backend/apps/model_managment_app.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import logging
2-
from typing import Optional
2+
from typing import Optional, List
33

44
from fastapi import Query, APIRouter, Header, Body
55

@@ -210,6 +210,26 @@ async def update_single_model(request: dict, authorization: Optional[str] = Head
210210
)
211211

212212

213+
@router.post("/batch_update_models", response_model=ModelResponse)
214+
async def batch_update_models(request: List[dict], authorization: Optional[str] = Header(None)):
215+
try:
216+
user_id, tenant_id = get_current_user_id(authorization)
217+
model_list = request
218+
for model in model_list:
219+
update_model_record(model["model_id"], model, user_id)
220+
return ModelResponse(
221+
code=200,
222+
message=f"Batch update models successfully",
223+
data=None
224+
)
225+
except Exception as e:
226+
return ModelResponse(
227+
code=500,
228+
message=f"Failed to batch update models: {str(e)}",
229+
data=None
230+
)
231+
232+
213233
@router.post("/delete", response_model=ModelResponse)
214234
async def delete_model(display_name: str = Query(..., embed=True), authorization: Optional[str] = Header(None)):
215235
"""

frontend/app/[locale]/setup/modelSetup/model/ModelDeleteDialog.tsx

Lines changed: 137 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { Modal, Button,Switch, App } from 'antd'
2-
import { DeleteOutlined, ExclamationCircleFilled, RightOutlined } from '@ant-design/icons'
2+
import { DeleteOutlined, ExclamationCircleFilled, RightOutlined, ReloadOutlined } from '@ant-design/icons'
33
import { useState } from 'react'
44
import { ModelOption, ModelType, ModelSource } from '@/types/config'
55
import { modelService } from '@/services/modelService'
6-
import { ModelEditDialog } from './ModelEditDialog'
6+
import { ModelEditDialog, ProviderConfigEditDialog } from './ModelEditDialog'
77
import { useConfig } from '@/hooks/useConfig'
88
import { useTranslation } from 'react-i18next'
99

@@ -30,6 +30,8 @@ export const ModelDeleteDialog = ({
3030
const [providerModels, setProviderModels] = useState<any[]>([])
3131
const [pendingSelectedProviderIds, setPendingSelectedProviderIds] = useState<Set<string>>(new Set())
3232
const [loadingSource, setLoadingSource] = useState<ModelSource | null>(null)
33+
const [isProviderConfigOpen, setIsProviderConfigOpen] = useState<boolean>(false)
34+
const [isConfirmLoading, setIsConfirmLoading] = useState<boolean>(false)
3335

3436
// 获取模型的颜色方案
3537
const getModelColorScheme = (type: ModelType): { bg: string; text: string; border: string } => {
@@ -187,6 +189,11 @@ export const ModelDeleteDialog = ({
187189
} finally {
188190
setLoadingSource(null)
189191
}
192+
} else if (source === 'openai') {
193+
// For OpenAI source, just set the selected source without prefetching
194+
// TODO: Call the relevant API to fetch OpenAI models
195+
setSelectedSource(source)
196+
return
190197
}
191198
setSelectedSource(source)
192199
}
@@ -284,6 +291,43 @@ export const ModelDeleteDialog = ({
284291
onClose()
285292
}
286293

294+
// Handle provider config save
295+
const handleProviderConfigSave = async ({ apiKey, maxTokens }: { apiKey: string; maxTokens: number }) => {
296+
if (selectedSource === 'silicon' && deletingModelType) {
297+
try {
298+
const currentIds = new Set(
299+
customModels
300+
.filter(m => m.type === deletingModelType && m.source === 'silicon')
301+
.map(m => m.name)
302+
)
303+
304+
// Build payload items for the current silicon models in required format
305+
const currentModelPayloads = customModels
306+
.filter(m => m.type === deletingModelType && m.source === 'silicon' && currentIds.has(m.name))
307+
.map(m => ({
308+
model_id: m.id,
309+
apiKey: apiKey || m.apiKey,
310+
maxTokens: maxTokens || m.maxTokens,
311+
}))
312+
313+
const result = await modelService.updateBatchModel(currentModelPayloads)
314+
315+
if (result.code !== 200) {
316+
message.error(t('model.dialog.error.noModelsFetched'))
317+
} else {
318+
message.success(t('model.dialog.success.updateSuccess'))
319+
}
320+
321+
// Optionally use currentModelPayloads for subsequent API calls if needed
322+
323+
} catch (e) {
324+
message.error(t('model.dialog.error.noModelsFetched'))
325+
}
326+
}
327+
await onSuccess()
328+
setIsProviderConfigOpen(false)
329+
}
330+
287331
return (
288332
// Refactor: Styles are embedded within the component
289333
<Modal
@@ -294,42 +338,60 @@ export const ModelDeleteDialog = ({
294338
<Button key="close" onClick={handleClose}>
295339
{t('common.button.close')}
296340
</Button>,
297-
<Button key="confirm" type="primary" onClick={async () => {
298-
// Only apply changes when silicon source is selected
299-
if (selectedSource === 'silicon' && deletingModelType) {
341+
// Only show confirm button for silicon and openai sources, not for OpenAI-API-Compatible
342+
(selectedSource !== "OpenAI-API-Compatible") && (
343+
<Button key="confirm" type="primary" loading={isConfirmLoading} onClick={async () => {
344+
setIsConfirmLoading(true)
300345
try {
301-
// Get all currently enabled models (including originally enabled and newly enabled ones)
302-
const allEnabledModels = providerModels.filter((pm: any) =>
303-
pendingSelectedProviderIds.has(pm.id)
304-
)
346+
// Handle changes for both silicon and openai sources
347+
if (selectedSource === 'silicon' && deletingModelType) {
348+
try {
349+
// Get all currently enabled models (including originally enabled and newly enabled ones)
350+
const allEnabledModels = providerModels.filter((pm: any) =>
351+
pendingSelectedProviderIds.has(pm.id)
352+
)
305353

306-
if (allEnabledModels.length > 0) {
307-
const apiKey = getApiKeyByType(deletingModelType)
308-
// Pass all currently enabled models
309-
await modelService.addBatchCustomModel({
310-
api_key: apiKey && apiKey.trim() !== '' ? apiKey : 'sk-no-api-key',
311-
provider: 'silicon',
312-
type: deletingModelType,
313-
max_tokens: 0,
314-
models: allEnabledModels
315-
})
316-
}
354+
if (allEnabledModels.length > 0) {
355+
const apiKey = getApiKeyByType(deletingModelType)
356+
// Pass all currently enabled models
357+
await modelService.addBatchCustomModel({
358+
api_key: apiKey && apiKey.trim() !== '' ? apiKey : 'sk-no-api-key',
359+
provider: 'silicon',
360+
type: deletingModelType,
361+
max_tokens: 0,
362+
models: allEnabledModels
363+
})
364+
}
317365

318-
// Refresh list
319-
await onSuccess()
320-
// Re-fetch provider models and sync switch states
321-
await prefetchSiliconProviderModels(deletingModelType)
322-
message.success('Update successful')
323-
// Close dialog
324-
handleClose()
325-
} catch (e) {
326-
console.error('Failed to apply model updates', e)
327-
message.error(t('model.dialog.error.addFailed', { error: e as any }))
366+
// Refresh list
367+
await onSuccess()
368+
// Re-fetch provider models and sync switch states
369+
await prefetchSiliconProviderModels(deletingModelType)
370+
message.success(t('model.dialog.success.updateSuccess'))
371+
// Close dialog
372+
handleClose()
373+
} catch (e) {
374+
console.error('Failed to apply model updates', e)
375+
message.error(t('model.dialog.error.addFailed', { error: e as any }))
376+
}
377+
} else if (selectedSource === 'openai' && deletingModelType) {
378+
try {
379+
// For OpenAI source, just refresh the list and close dialog
380+
await onSuccess()
381+
message.success(t('model.dialog.success.updateSuccess'))
382+
handleClose()
383+
} catch (e) {
384+
console.error('Failed to apply OpenAI model updates', e)
385+
message.error(t('model.dialog.error.addFailed', { error: e as any }))
386+
}
387+
}
388+
} finally {
389+
setIsConfirmLoading(false)
328390
}
329-
}
330-
}} disabled={selectedSource !== 'silicon'}>
331-
{t('common.confirm')}
332-
</Button>,
391+
}}>
392+
{t('common.confirm')}
393+
</Button>
394+
),
333395
]}
334396
width={520}
335397
destroyOnClose
@@ -458,7 +520,7 @@ export const ModelDeleteDialog = ({
458520
</div>
459521
) : (
460522
<div>
461-
<div className="flex items-center mb-4">
523+
<div className="flex items-center justify-between mb-4">
462524
<button
463525
onClick={() => { setSelectedSource(null); setProviderModels([]) }}
464526
className="text-blue-500 hover:text-blue-700 flex items-center"
@@ -477,6 +539,30 @@ export const ModelDeleteDialog = ({
477539
</svg>
478540
{t('common.back')}
479541
</button>
542+
543+
{selectedSource !== 'OpenAI-API-Compatible' && (
544+
<div className="flex gap-2">
545+
<Button
546+
size="small"
547+
icon={<ReloadOutlined className="text-blue-500" />}
548+
onClick={async () => {
549+
if (selectedSource === 'silicon' && deletingModelType) {
550+
try {
551+
await prefetchSiliconProviderModels(deletingModelType)
552+
message.success(t('common.message.refreshSuccess'))
553+
} catch (error) {
554+
message.error(t('common.message.refreshFailed'))
555+
}
556+
}
557+
}}
558+
className="border-none shadow-none hover:bg-blue-50"
559+
>
560+
</Button>
561+
<Button size="small" onClick={() => setIsProviderConfigOpen(true)}>
562+
{t('common.button.editConfig')}
563+
</Button>
564+
</div>
565+
)}
480566
</div>
481567

482568
{selectedSource === 'silicon' && providerModels.length > 0 ? (
@@ -520,8 +606,10 @@ export const ModelDeleteDialog = ({
520606
.filter((model) => model.type === deletingModelType && model.source === selectedSource)
521607
.map((model) => (
522608
<div key={model.name}
523-
onClick={() => handleEditModel(model)}
524-
className="p-2 flex justify-between items-center hover:bg-gray-50 text-sm cursor-pointer">
609+
onClick={selectedSource === 'OpenAI-API-Compatible' ? () => handleEditModel(model) : undefined}
610+
className={`p-2 flex justify-between items-center hover:bg-gray-50 text-sm ${
611+
selectedSource === 'OpenAI-API-Compatible' ? 'cursor-pointer' : ''
612+
}`}>
525613
<div className="flex-1 min-w-0">
526614
<div className="font-medium truncate" title={model.name}>
527615
{model.displayName || model.name} ({model.name})
@@ -583,7 +671,10 @@ export const ModelDeleteDialog = ({
583671
<p className="font-bold text-medium">{t('common.notice')}</p>
584672
</div>
585673
<p className="mt-0.5 ml-6">
586-
{t('model.dialog.delete.warning')}
674+
{selectedSource === 'OpenAI-API-Compatible'
675+
? t('model.dialog.delete.warning')
676+
: t('model.dialog.edit.warning')
677+
}
587678
</p>
588679
</div>
589680
</div>
@@ -602,6 +693,14 @@ export const ModelDeleteDialog = ({
602693
}
603694
}}
604695
/>
696+
<ProviderConfigEditDialog
697+
isOpen={isProviderConfigOpen}
698+
onClose={() => setIsProviderConfigOpen(false)}
699+
initialApiKey={getApiKeyByType(deletingModelType)}
700+
initialMaxTokens={(customModels.find(m => m.type === deletingModelType && m.source === 'silicon')?.maxTokens || 4096).toString()}
701+
modelType={deletingModelType || undefined}
702+
onSave={handleProviderConfigSave}
703+
/>
605704
</Modal>
606705
)
607706
}

frontend/app/[locale]/setup/modelSetup/model/ModelEditDialog.tsx

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,4 +179,85 @@ export const ModelEditDialog = ({ isOpen, model, onClose, onSuccess }: ModelEdit
179179
</div>
180180
</Modal>
181181
)
182+
}
183+
184+
// New: provider config edit dialog (only apiKey and maxTokens)
185+
interface ProviderConfigEditDialogProps {
186+
isOpen: boolean
187+
initialApiKey?: string
188+
initialMaxTokens?: string
189+
modelType?: ModelType
190+
onClose: () => void
191+
onSave: (config: { apiKey: string; maxTokens: number }) => Promise<void> | void
192+
}
193+
194+
export const ProviderConfigEditDialog = ({
195+
isOpen,
196+
initialApiKey = '',
197+
initialMaxTokens = '4096',
198+
modelType,
199+
onClose,
200+
onSave,
201+
}: ProviderConfigEditDialogProps) => {
202+
const { t } = useTranslation()
203+
const [apiKey, setApiKey] = useState<string>(initialApiKey)
204+
const [maxTokens, setMaxTokens] = useState<string>(initialMaxTokens)
205+
const [saving, setSaving] = useState<boolean>(false)
206+
207+
useEffect(() => {
208+
setApiKey(initialApiKey)
209+
setMaxTokens(initialMaxTokens)
210+
}, [initialApiKey, initialMaxTokens])
211+
212+
const valid = () => {
213+
const parsed = parseInt(maxTokens)
214+
return !Number.isNaN(parsed) && parsed >= 0
215+
}
216+
217+
const handleSave = async () => {
218+
if (!valid()) return
219+
try {
220+
setSaving(true)
221+
await onSave({ apiKey: apiKey.trim() === '' ? 'sk-no-api-key' : apiKey, maxTokens: parseInt(maxTokens) })
222+
message.success(t('common.success') || '保存成功')
223+
onClose()
224+
} finally {
225+
setSaving(false)
226+
}
227+
}
228+
229+
const isEmbeddingModel = modelType === "embedding" || modelType === "multi_embedding"
230+
231+
return (
232+
<Modal
233+
title={t('common.button.editConfig')}
234+
open={isOpen}
235+
onCancel={onClose}
236+
footer={null}
237+
destroyOnClose
238+
>
239+
<div className="space-y-4">
240+
<div>
241+
<label className="block mb-1 text-sm font-medium text-gray-700">
242+
{t('model.dialog.label.apiKey')}
243+
</label>
244+
<Input.Password value={apiKey} onChange={(e) => setApiKey(e.target.value)} />
245+
</div>
246+
{!isEmbeddingModel && (
247+
<div>
248+
<label className="block mb-1 text-sm font-medium text-gray-700">
249+
{t('model.dialog.label.maxTokens')}
250+
</label>
251+
<Input value={maxTokens} onChange={(e) => setMaxTokens(e.target.value)} />
252+
</div>
253+
)}
254+
<div className="flex justify-end space-x-3">
255+
<Button onClick={onClose}>{t('common.button.cancel')}</Button>
256+
<Button type="primary" onClick={handleSave} loading={saving} disabled={!valid()}>
257+
{t('common.button.save')}
258+
</Button>
259+
</div>
260+
</div>
261+
</Modal>
262+
)
182263
}

0 commit comments

Comments
 (0)