diff --git a/controller/channel-test.go b/controller/channel-test.go index 37bf422b1ce..efe10a49fb3 100644 --- a/controller/channel-test.go +++ b/controller/channel-test.go @@ -74,7 +74,7 @@ func resolveChannelTestUserID(c *gin.Context) (int, error) { return rootUser.Id, nil } -func testChannel(channel *model.Channel, testUserID int, testModel string, endpointType string, isStream bool) testResult { +func testChannel(channel *model.Channel, testUserID int, testModel string, endpointType string, isStream bool, keyIndex *int) testResult { tik := time.Now() var unsupportedTestChannelTypes = []int{ constant.ChannelTypeMidjourney, @@ -176,6 +176,9 @@ func testChannel(channel *model.Channel, testUserID int, testModel string, endpo c.Set("base_url", channel.GetBaseURL()) group, _ := model.GetUserGroup(testUserID, false) c.Set("group", group) + if keyIndex != nil { + middleware.SetChannelTestMultiKeyIndex(c, *keyIndex) + } newAPIError := middleware.SetupContextForSelectedChannel(c, channel, testModel) if newAPIError != nil { @@ -851,13 +854,27 @@ func TestChannel(c *gin.Context) { testModel := c.Query("model") endpointType := c.Query("endpoint_type") isStream, _ := strconv.ParseBool(c.Query("stream")) + var keyIndex *int + keyIndexText := strings.TrimSpace(c.Query("key_index")) + if keyIndexText != "" { + parsedKeyIndex, err := strconv.Atoi(keyIndexText) + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": "key_index must be an integer", + "time": 0.0, + }) + return + } + keyIndex = &parsedKeyIndex + } testUserID, err := resolveChannelTestUserID(c) if err != nil { common.ApiError(c, err) return } tik := time.Now() - result := testChannel(channel, testUserID, testModel, endpointType, isStream) + result := testChannel(channel, testUserID, testModel, endpointType, isStream, keyIndex) if result.localErr != nil { resp := gin.H{ "success": false, @@ -928,7 +945,7 @@ func testAllChannels(notify bool) error { } isChannelEnabled := channel.Status == common.ChannelStatusEnabled tik := time.Now() - result := testChannel(channel, testUserID, "", "", shouldUseStreamForAutomaticChannelTest(channel)) + result := testChannel(channel, testUserID, "", "", shouldUseStreamForAutomaticChannelTest(channel), nil) tok := time.Now() milliseconds := tok.Sub(tik).Milliseconds() diff --git a/controller/channel.go b/controller/channel.go index 5b1cefef53b..a85bab731b1 100644 --- a/controller/channel.go +++ b/controller/channel.go @@ -1336,7 +1336,7 @@ type KeyStatus struct { Status int `json:"status"` // 1: enabled, 2: disabled DisabledTime int64 `json:"disabled_time,omitempty"` Reason string `json:"reason,omitempty"` - KeyPreview string `json:"key_preview"` // first 10 chars of key for identification + KeyPreview string `json:"key_preview"` // masked key for identification } // ManageMultiKeys handles multi-key management operations @@ -1428,18 +1428,12 @@ func ManageMultiKeys(c *gin.Context) { } } - // Create key preview (first 10 chars) - keyPreview := key - if len(key) > 10 { - keyPreview = key[:10] + "..." - } - allKeyStatusList = append(allKeyStatusList, KeyStatus{ Index: i, Status: status, DisabledTime: disabledTime, Reason: reason, - KeyPreview: keyPreview, + KeyPreview: model.MaskTokenKey(key), }) } diff --git a/middleware/distributor.go b/middleware/distributor.go index cf5caa06d51..b3e4af47a8d 100644 --- a/middleware/distributor.go +++ b/middleware/distributor.go @@ -440,6 +440,21 @@ func getTaskOriginModelName(c *gin.Context) string { return "" } +const ginKeyChannelTestMultiKeyIndex = "channel_test_multi_key_index" + +func SetChannelTestMultiKeyIndex(c *gin.Context, index int) { + c.Set(ginKeyChannelTestMultiKeyIndex, index) +} + +func getChannelTestMultiKeyIndex(c *gin.Context) (int, bool) { + value, ok := c.Get(ginKeyChannelTestMultiKeyIndex) + if !ok { + return 0, false + } + index, ok := value.(int) + return index, ok +} + func SetupContextForSelectedChannel(c *gin.Context, channel *model.Channel, modelName string) *types.NewAPIError { c.Set("original_model", modelName) // for retry if channel == nil { @@ -465,9 +480,27 @@ func SetupContextForSelectedChannel(c *gin.Context, channel *model.Channel, mode common.SetContextKey(c, constant.ContextKeyChannelModelMapping, channel.GetModelMapping()) common.SetContextKey(c, constant.ContextKeyChannelStatusCodeMapping, channel.GetStatusCodeMapping()) - key, index, newAPIError := channel.GetNextEnabledKey() - if newAPIError != nil { - return newAPIError + var key string + var index int + var newAPIError *types.NewAPIError + if testKeyIndex, ok := getChannelTestMultiKeyIndex(c); ok { + if !channel.ChannelInfo.IsMultiKey { + return types.NewError(errors.New("key_index is only supported for multi-key channels"), types.ErrorCodeChannelNoAvailableKey, types.ErrOptionWithSkipRetry()) + } + selectedKey, selectedIndex, reason, valid := channel.GetEnabledKeyByIndex(testKeyIndex) + if !valid { + if reason == "" { + reason = "specified_key_unavailable" + } + return types.NewError(fmt.Errorf("specified key unavailable: %s", reason), types.ErrorCodeChannelNoAvailableKey, types.ErrOptionWithSkipRetry()) + } + key = selectedKey + index = selectedIndex + } else { + key, index, newAPIError = channel.GetNextEnabledKey() + if newAPIError != nil { + return newAPIError + } } if channel.ChannelInfo.IsMultiKey { common.SetContextKey(c, constant.ContextKeyChannelIsMultiKey, true) diff --git a/model/channel.go b/model/channel.go index 725b8975262..20582bdc6e5 100644 --- a/model/channel.go +++ b/model/channel.go @@ -282,6 +282,26 @@ func (channel *Channel) GetNextEnabledKey() (string, int, *types.NewAPIError) { } } +func (channel *Channel) GetEnabledKeyByIndex(index int) (string, int, string, bool) { + if index < 0 { + return "", index, "invalid_key_index", false + } + + keys := channel.GetKeys() + if index >= len(keys) { + return "", index, "key_index_out_of_range", false + } + + statusList := channel.ChannelInfo.MultiKeyStatusList + if statusList != nil { + if status, ok := statusList[index]; ok && status != common.ChannelStatusEnabled { + return "", index, fmt.Sprintf("key_status_%d", status), false + } + } + + return keys[index], index, "", true +} + func (channel *Channel) SaveChannelInfo() error { return DB.Model(channel).Update("channel_info", channel.ChannelInfo).Error } diff --git a/web/default/src/features/channels/api.ts b/web/default/src/features/channels/api.ts index 303d97cd3b8..1f3970d414b 100644 --- a/web/default/src/features/channels/api.ts +++ b/web/default/src/features/channels/api.ts @@ -181,7 +181,12 @@ export async function batchSetChannelTag( */ export async function testChannel( id: number, - params?: { model?: string; endpoint_type?: string; stream?: boolean } + params?: { + model?: string + endpoint_type?: string + stream?: boolean + key_index?: number + } ): Promise { const res = await api.get( `/api/channel/test/${id}`, diff --git a/web/default/src/features/channels/components/dialogs/channel-test-dialog.tsx b/web/default/src/features/channels/components/dialogs/channel-test-dialog.tsx index a7844d2e32f..097b2f507c1 100644 --- a/web/default/src/features/channels/components/dialogs/channel-test-dialog.tsx +++ b/web/default/src/features/channels/components/dialogs/channel-test-dialog.tsx @@ -16,7 +16,14 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ -import { type ChangeEvent, useCallback, useMemo, useRef, useState } from 'react' +import { + type ChangeEvent, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react' import { useQueryClient } from '@tanstack/react-query' import { type ColumnDef, @@ -78,15 +85,17 @@ import { sideDrawerHeaderClassName, } from '@/components/drawer-layout' import { StatusBadge } from '@/components/status-badge' -import { updateChannel } from '../../api' +import { disableMultiKey, getMultiKeyStatus, updateChannel } from '../../api' import { channelsQueryKeys, formatResponseTime, handleTestChannel, } from '../../lib' +import { getMultiKeyStatusConfig } from '../../lib/multi-key-utils' import type { Channel, GetChannelsResponse, + KeyStatus, SearchChannelsResponse, } from '../../types' import { useChannels } from '../channels-provider' @@ -320,6 +329,13 @@ function ChannelTestDialogContent({ const batchStopRequestedRef = useRef(false) const [endpointType, setEndpointType] = useState('auto') const [isStreamTest, setIsStreamTest] = useState(false) + const [selectedTestKeyIndex, setSelectedTestKeyIndex] = useState('auto') + const [modelTestKeyStatuses, setModelTestKeyStatuses] = useState( + [] + ) + const [modelTestKeyStatusLoading, setModelTestKeyStatusLoading] = + useState(false) + const [disableTestKeyLoading, setDisableTestKeyLoading] = useState(false) const [searchTerm, setSearchTerm] = useState('') const [testResults, setTestResults] = useState>({}) const [rowSelection, setRowSelection] = useState({}) @@ -349,11 +365,43 @@ function ChannelTestDialogContent({ })), [t] ) + const isMultiKeyChannel = Boolean(currentRow.channel_info?.is_multi_key) + const selectedKeyIndex = + selectedTestKeyIndex === 'auto' ? undefined : Number(selectedTestKeyIndex) + const selectedResultKeyPart = + typeof selectedKeyIndex === 'number' && Number.isFinite(selectedKeyIndex) + ? `key-${selectedKeyIndex}` + : 'auto' + const keySelectItems = useMemo( + () => [ + { value: 'auto', label: t('Auto select'), disabled: false }, + ...modelTestKeyStatuses.map((item) => { + const statusConfig = getMultiKeyStatusConfig(item.status) + return { + value: String(item.index), + label: `#${item.index} · ${item.key_preview || '-'} · ${t( + statusConfig.label + )}`, + disabled: item.status !== 1, + } + }), + ], + [modelTestKeyStatuses, t] + ) + + const getModelTestResultKey = useCallback( + (model: string) => `${selectedResultKeyPart}-${model}`, + [selectedResultKeyPart] + ) const resetState = useCallback(() => { batchStopRequestedRef.current = true setEndpointType('auto') setIsStreamTest(false) + setSelectedTestKeyIndex('auto') + setModelTestKeyStatuses([]) + setModelTestKeyStatusLoading(false) + setDisableTestKeyLoading(false) setSearchTerm('') setTestResults({}) setRowSelection({}) @@ -407,13 +455,20 @@ function ChannelTestDialogContent({ ) const successModels = useMemo( - () => models.filter((model) => testResults[model]?.status === 'success'), - [models, testResults] + () => + models.filter( + (model) => + testResults[getModelTestResultKey(model)]?.status === 'success' + ), + [getModelTestResultKey, models, testResults] ) const failedModels = useMemo( - () => models.filter((model) => testResults[model]?.status === 'error'), - [models, testResults] + () => + models.filter( + (model) => testResults[getModelTestResultKey(model)]?.status === 'error' + ), + [getModelTestResultKey, models, testResults] ) const filteredModels = useMemo(() => { @@ -495,6 +550,76 @@ function ChannelTestDialogContent({ [queryClient, updateChannelTestCache] ) + const loadModelTestKeyStatuses = useCallback(async () => { + if (!isMultiKeyChannel) { + setModelTestKeyStatuses([]) + setSelectedTestKeyIndex('auto') + return + } + + setModelTestKeyStatusLoading(true) + try { + const pageSize = Math.max( + currentRow.channel_info?.multi_key_size ?? 50, + 50 + ) + const response = await getMultiKeyStatus(currentRow.id, 1, pageSize) + if (response.success) { + setModelTestKeyStatuses(response.data?.keys ?? []) + } else { + toast.error(response.message || t('Failed to get key status')) + } + } catch (error: unknown) { + toast.error( + error instanceof Error ? error.message : t('Failed to get key status') + ) + } finally { + setModelTestKeyStatusLoading(false) + } + }, [ + currentRow.channel_info?.multi_key_size, + currentRow.id, + isMultiKeyChannel, + t, + ]) + + useEffect(() => { + if (!open) return + void loadModelTestKeyStatuses() + }, [loadModelTestKeyStatuses, open]) + + const disableSelectedTestKey = useCallback(async () => { + if (typeof selectedKeyIndex !== 'number') { + toast.error(t('Please select a key to disable')) + return + } + + setDisableTestKeyLoading(true) + try { + const response = await disableMultiKey(currentRow.id, selectedKeyIndex) + if (response.success) { + toast.success(t('Key disabled')) + setSelectedTestKeyIndex('auto') + await loadModelTestKeyStatuses() + refreshChannelLists() + } else { + toast.error(response.message || t('Failed to disable key')) + } + } catch (error: unknown) { + toast.error( + error instanceof Error ? error.message : t('Failed to disable key') + ) + } finally { + setDisableTestKeyLoading(false) + } + }, [ + currentRow.id, + loadModelTestKeyStatuses, + refreshChannelLists, + selectedKeyIndex, + t, + ]) + const testSingleModel = useCallback( async ( model: string, @@ -503,8 +628,9 @@ function ChannelTestDialogContent({ ): Promise => { if (!currentRow) return - markModelTesting(model, true) - updateTestResult(model, { status: 'testing' }) + const testResultKey = getModelTestResultKey(model) + markModelTesting(testResultKey, true) + updateTestResult(testResultKey, { status: 'testing' }) let finalResult: TestResult | undefined try { @@ -515,6 +641,7 @@ function ChannelTestDialogContent({ testModel: model, endpointType: endpointType === 'auto' ? undefined : endpointType, stream: effectiveStreamTest || undefined, + keyIndex: selectedKeyIndex, silent, }, (success, responseTime, error, errorCode) => { @@ -526,7 +653,7 @@ function ChannelTestDialogContent({ error, errorCode, } - updateTestResult(model, finalResult) + updateTestResult(testResultKey, finalResult) } ) } catch (error: unknown) { @@ -535,9 +662,9 @@ function ChannelTestDialogContent({ completedAt: Date.now(), error: error instanceof Error ? error.message : t('Test failed'), } - updateTestResult(model, finalResult) + updateTestResult(testResultKey, finalResult) } finally { - markModelTesting(model, false) + markModelTesting(testResultKey, false) if (refreshList) { refreshChannelLists( createChannelTestCachePatch( @@ -553,8 +680,10 @@ function ChannelTestDialogContent({ currentRow, endpointType, effectiveStreamTest, + getModelTestResultKey, markModelTesting, refreshChannelLists, + selectedKeyIndex, t, updateTestResult, ] @@ -627,17 +756,18 @@ function ChannelTestDialogContent({ startIndex + BATCH_TEST_CONCURRENCY ) const batchPromises = batch.map(async (modelName) => { + const testResultKey = getModelTestResultKey(modelName) try { const result = await testSingleModel(modelName, true, false) const finalResult = result ?? createFallbackResult() if (!result) { - updateTestResult(modelName, finalResult) + updateTestResult(testResultKey, finalResult) } recordBatchResult(finalResult) return finalResult } catch (error: unknown) { const fallbackResult = createFallbackResult(error) - updateTestResult(modelName, fallbackResult) + updateTestResult(testResultKey, fallbackResult) recordBatchResult(fallbackResult) return fallbackResult } @@ -697,7 +827,13 @@ function ChannelTestDialogContent({ refreshChannelLists(resultPatch) } }, - [refreshChannelLists, t, testSingleModel, updateTestResult] + [ + getModelTestResultKey, + refreshChannelLists, + t, + testSingleModel, + updateTestResult, + ] ) const handleSelectSuccessfulModels = useCallback(() => { @@ -712,7 +848,7 @@ function ChannelTestDialogContent({ const handleDeleteFailedModels = useCallback(async () => { const failed = models.filter( - (model) => testResults[model]?.status === 'error' + (model) => testResults[getModelTestResultKey(model)]?.status === 'error' ) if (!failed.length) { setIsDeleteFailedDialogOpen(false) @@ -735,7 +871,7 @@ function ChannelTestDialogContent({ }) setTestResults((prev) => { const next = { ...prev } - for (const model of failed) delete next[model] + for (const model of failed) delete next[getModelTestResultKey(model)] return next }) setRowSelection((prev) => { @@ -760,7 +896,14 @@ function ChannelTestDialogContent({ } finally { setIsDeletingFailed(false) } - }, [currentRow.id, models, refreshChannelLists, t, testResults]) + }, [ + currentRow.id, + getModelTestResultKey, + models, + refreshChannelLists, + t, + testResults, + ]) const handleClose = useCallback(() => { resetState() @@ -838,11 +981,19 @@ function ChannelTestDialogContent({ header: t('Status'), cell: ({ row }) => { const model = row.original.model - const result = testResults[model] + const testResultKey = getModelTestResultKey(model) + const result = testResults[testResultKey] return ( ) @@ -855,7 +1006,7 @@ function ChannelTestDialogContent({ header: t('Actions'), cell: ({ row }) => { const model = row.original.model - const isTestingModel = testingModels.has(model) + const isTestingModel = testingModels.has(getModelTestResultKey(model)) return ( )} + {canDisableTestKey && ( + + )} ) @@ -1343,11 +1581,7 @@ function FailureDetailsSheet({ ) } -function TestModelsBulkActions({ - table, -}: { - table: TanStackTable -}) { +function TestModelsBulkActions({ table }: { table: TanStackTable }) { const { t } = useTranslation() const { copyToClipboard } = useCopyToClipboard() const selectedRows = table.getFilteredSelectedRowModel().rows diff --git a/web/default/src/features/channels/lib/channel-actions.ts b/web/default/src/features/channels/lib/channel-actions.ts index f32bf520a01..ef39fa76ccb 100644 --- a/web/default/src/features/channels/lib/channel-actions.ts +++ b/web/default/src/features/channels/lib/channel-actions.ts @@ -270,6 +270,7 @@ export async function handleTestChannel( testModel?: string endpointType?: string stream?: boolean + keyIndex?: number silent?: boolean }, onTestComplete?: ( @@ -280,13 +281,20 @@ export async function handleTestChannel( ) => void ): Promise { const payload = - options && (options.testModel || options.endpointType || options.stream) + options && + (options.testModel || + options.endpointType || + options.stream || + typeof options.keyIndex === 'number') ? { ...(options.testModel ? { model: options.testModel } : {}), ...(options.endpointType ? { endpoint_type: options.endpointType } : {}), ...(options.stream ? { stream: true } : {}), + ...(typeof options.keyIndex === 'number' + ? { key_index: options.keyIndex } + : {}), } : undefined diff --git a/web/default/src/i18n/locales/en.json b/web/default/src/i18n/locales/en.json index 5b907ec7ba1..bd8b0e7dc53 100644 --- a/web/default/src/i18n/locales/en.json +++ b/web/default/src/i18n/locales/en.json @@ -475,6 +475,7 @@ "Auto group behavior": "Auto group behavior", "Auto Group Chain": "Auto Group Chain", "Auto refresh": "Auto refresh", + "Auto select": "Auto select", "Auto Sync Upstream Models": "Auto Sync Upstream Models", "Auto-disable rules": "Auto-disable rules", "Auto-disable status codes": "Auto-disable status codes", @@ -742,6 +743,7 @@ "checkout.session.completed": "checkout.session.completed", "checkout.session.expired": "checkout.session.expired", "Chinese": "Chinese", + "Choose a specific enabled key for this test.": "Choose a specific enabled key for this test.", "Choose a username": "Choose a username", "Choose an amount and payment method": "Choose an amount and payment method", "Choose between default expanded, compact icon-only, or full layout mode": "Choose between default expanded, compact icon-only, or full layout mode", @@ -1284,6 +1286,7 @@ "Disable": "Disable", "Disable 2FA": "Disable 2FA", "Disable All": "Disable All", + "Disable current key": "Disable current key", "Disable on failure": "Disable on failure", "Disable selected channels": "Disable selected channels", "Disable selected models": "Disable selected models", @@ -1708,6 +1711,7 @@ "Failed to disable {{count}} model(s)": "Failed to disable {{count}} model(s)", "Failed to disable 2FA": "Failed to disable 2FA", "Failed to disable channels": "Failed to disable channels", + "Failed to disable key": "Failed to disable key", "Failed to disable model": "Failed to disable model", "Failed to disable tag channels": "Failed to disable tag channels", "Failed to discover OIDC endpoints": "Failed to discover OIDC endpoints", @@ -1728,6 +1732,7 @@ "Failed to fetch user information": "Failed to fetch user information", "Failed to fix abilities": "Failed to fix abilities", "Failed to generate token": "Failed to generate token", + "Failed to get key status": "Failed to get key status", "Failed to initialize OAuth": "Failed to initialize OAuth", "Failed to initialize system": "Failed to initialize system", "Failed to load": "Failed to load", @@ -2255,6 +2260,7 @@ "Keep this above 1 minute to avoid heavy database load": "Keep this above 1 minute to avoid heavy database load", "Keep-alive Ping": "Keep-alive Ping", "Key": "Key", + "Key disabled": "Key disabled", "Key Fingerprint": "Key Fingerprint", "Key Sources": "Key Sources", "Key Summary": "Key Summary", @@ -3160,6 +3166,7 @@ "Please fix the highlighted fields before saving": "Please fix the highlighted fields before saving", "Please log in with the appropriate credentials": "Please log in with the appropriate credentials", "Please select a container": "Please select a container", + "Please select a key to disable": "Please select a key to disable", "Please select a payment method": "Please select a payment method", "Please select a primary model": "Please select a primary model", "Please select a subscription plan": "Please select a subscription plan", @@ -3871,11 +3878,11 @@ "Shorten": "Shorten", "Show": "Show", "Show All": "Show All", - "Show sensitive data": "Show sensitive data", "Show all providers including unbound": "Show all providers including unbound", "Show only bound providers": "Show only bound providers", "Show or hide flow columns": "Show or hide flow columns", "Show prices in currency instead of quota.": "Show prices in currency instead of quota.", + "Show sensitive data": "Show sensitive data", "Show setup guide": "Show setup guide", "Show token usage statistics in the UI": "Show token usage statistics in the UI", "Showcase core capabilities with demo credentials and limited access.": "Showcase core capabilities with demo credentials and limited access.", @@ -4124,6 +4131,7 @@ "Test connectivity for:": "Test connectivity for:", "Test failed": "Test failed", "Test interval (minutes)": "Test interval (minutes)", + "Test Key": "Test Key", "Test Latency": "Test Latency", "Test Mode": "Test Mode", "Test Model": "Test Model", diff --git a/web/default/src/i18n/locales/fr.json b/web/default/src/i18n/locales/fr.json index 2d08182f44e..3b9168f324a 100644 --- a/web/default/src/i18n/locales/fr.json +++ b/web/default/src/i18n/locales/fr.json @@ -475,6 +475,7 @@ "Auto group behavior": "Comportement du groupe auto", "Auto Group Chain": "Chaîne de groupes automatique", "Auto refresh": "Actualisation automatique", + "Auto select": "Sélection automatique", "Auto Sync Upstream Models": "Synchronisation automatique des modèles en amont", "Auto-disable rules": "Règles de désactivation automatique", "Auto-disable status codes": "Codes de statut de désactivation auto", @@ -742,6 +743,7 @@ "checkout.session.completed": "checkout.session.completed", "checkout.session.expired": "checkout.session.expired", "Chinese": "Chinois", + "Choose a specific enabled key for this test.": "Choisissez une clé activée spécifique pour ce test.", "Choose a username": "Choisir un nom d'utilisateur", "Choose an amount and payment method": "Choisir un montant et un mode de paiement", "Choose between default expanded, compact icon-only, or full layout mode": "Choisissez entre le mode d'affichage étendu par défaut, compact (icône uniquement) ou complet", @@ -1284,6 +1286,7 @@ "Disable": "Désactiver", "Disable 2FA": "Désactiver la 2FA", "Disable All": "Désactiver tout", + "Disable current key": "Désactiver la clé actuelle", "Disable on failure": "Désactiver en cas d'échec", "Disable selected channels": "Désactiver les canaux sélectionnés", "Disable selected models": "Désactiver les modèles sélectionnés", @@ -1708,6 +1711,7 @@ "Failed to disable {{count}} model(s)": "Échec de la désactivation de {{count}} modèle(s)", "Failed to disable 2FA": "Échec de la désactivation de la 2FA", "Failed to disable channels": "Échec de la désactivation des canaux", + "Failed to disable key": "Échec de la désactivation de la clé", "Failed to disable model": "Échec de la désactivation du modèle", "Failed to disable tag channels": "Échec de la désactivation des canaux de tags", "Failed to discover OIDC endpoints": "Échec de la découverte des points de terminaison OIDC", @@ -1728,6 +1732,7 @@ "Failed to fetch user information": "Échec de la récupération des informations utilisateur", "Failed to fix abilities": "Échec de la correction des capacités", "Failed to generate token": "Échec de la génération du jeton", + "Failed to get key status": "Échec de la récupération de l’état de la clé", "Failed to initialize OAuth": "Échec de l'initialisation d'OAuth", "Failed to initialize system": "Échec de l'initialisation du système", "Failed to load": "Échec du chargement", @@ -2255,6 +2260,7 @@ "Keep this above 1 minute to avoid heavy database load": "Gardez cette valeur au-dessus de 1 minute pour éviter une charge excessive de la base de données", "Keep-alive Ping": "Ping de maintien de connexion", "Key": "Clé", + "Key disabled": "Clé désactivée", "Key Fingerprint": "Empreinte de clé", "Key Sources": "Sources de clé", "Key Summary": "Résumé de clé", @@ -3160,6 +3166,7 @@ "Please fix the highlighted fields before saving": "Veuillez corriger les champs en surbrillance avant d’enregistrer", "Please log in with the appropriate credentials": "Veuillez vous connecter avec les identifiants appropriés", "Please select a container": "Veuillez sélectionner un conteneur", + "Please select a key to disable": "Veuillez sélectionner une clé à désactiver", "Please select a payment method": "Veuillez sélectionner un mode de paiement", "Please select a primary model": "Veuillez sélectionner un modèle principal", "Please select a subscription plan": "Veuillez sélectionner un plan d'abonnement", @@ -3871,11 +3878,11 @@ "Shorten": "Raccourcir", "Show": "Afficher", "Show All": "Tout afficher", - "Show sensitive data": "Afficher les données sensibles", "Show all providers including unbound": "Afficher tous les fournisseurs (y compris non liés)", "Show only bound providers": "Afficher uniquement les fournisseurs liés", "Show or hide flow columns": "Afficher ou masquer les colonnes du flux", "Show prices in currency instead of quota.": "Afficher les prix en devise au lieu du quota.", + "Show sensitive data": "Afficher les données sensibles", "Show setup guide": "Afficher le guide de configuration", "Show token usage statistics in the UI": "Afficher les statistiques d'utilisation des jetons dans l'interface utilisateur", "Showcase core capabilities with demo credentials and limited access.": "Présenter les fonctionnalités principales avec des identifiants de démonstration et un accès limité.", @@ -4124,6 +4131,7 @@ "Test connectivity for:": "Tester la connectivité pour :", "Test failed": "Échec du test", "Test interval (minutes)": "Intervalle de test (minutes)", + "Test Key": "Clé de test", "Test Latency": "Tester la latence", "Test Mode": "Mode test", "Test Model": "Tester le modèle", diff --git a/web/default/src/i18n/locales/ja.json b/web/default/src/i18n/locales/ja.json index 7af0d8eadf1..d983c40190b 100644 --- a/web/default/src/i18n/locales/ja.json +++ b/web/default/src/i18n/locales/ja.json @@ -475,6 +475,7 @@ "Auto group behavior": "auto グループの動作", "Auto Group Chain": "自動グループチェーン", "Auto refresh": "自動更新", + "Auto select": "自動選択", "Auto Sync Upstream Models": "アップストリームモデルの自動同期", "Auto-disable rules": "自動無効化ルール", "Auto-disable status codes": "自動無効化するステータスコード", @@ -742,6 +743,7 @@ "checkout.session.completed": "checkout.session.completed", "checkout.session.expired": "checkout.session.expired", "Chinese": "中国語", + "Choose a specific enabled key for this test.": "このテストで使用する有効なキーを指定します。", "Choose a username": "ユーザー名を選択", "Choose an amount and payment method": "金額と支払い方法を選択してください", "Choose between default expanded, compact icon-only, or full layout mode": "デフォルトの展開表示、コンパクトなアイコンのみ、またはフルレイアウトモードから選択します", @@ -1284,6 +1286,7 @@ "Disable": "無効にする", "Disable 2FA": "2FAを無効にする", "Disable All": "すべて無効にする", + "Disable current key": "現在のキーを無効化", "Disable on failure": "失敗時に無効にする", "Disable selected channels": "選択したチャネルを無効にする", "Disable selected models": "選択したモデルを無効にする", @@ -1708,6 +1711,7 @@ "Failed to disable {{count}} model(s)": "{{count}} 個のモデルの無効化に失敗しました", "Failed to disable 2FA": "2FAの無効化に失敗しました", "Failed to disable channels": "チャネルの無効化に失敗しました", + "Failed to disable key": "キーの無効化に失敗しました", "Failed to disable model": "モデルの無効化に失敗しました", "Failed to disable tag channels": "タグチャネルの無効化に失敗しました", "Failed to discover OIDC endpoints": "OIDCエンドポイントの検出に失敗しました", @@ -1728,6 +1732,7 @@ "Failed to fetch user information": "ユーザー情報の取得に失敗しました", "Failed to fix abilities": "アビリティの修正に失敗しました", "Failed to generate token": "トークンの生成に失敗しました", + "Failed to get key status": "キー状態の取得に失敗しました", "Failed to initialize OAuth": "OAuthの初期化に失敗しました", "Failed to initialize system": "システムの初期化に失敗しました", "Failed to load": "読み込みに失敗しました", @@ -2255,6 +2260,7 @@ "Keep this above 1 minute to avoid heavy database load": "データベースへの負荷を避けるため、これを1分以上に保ってください", "Keep-alive Ping": "キープアライブPing", "Key": "キー", + "Key disabled": "キーを無効化しました", "Key Fingerprint": "キーフィンガープリント", "Key Sources": "キーソース", "Key Summary": "キー概要", @@ -3160,6 +3166,7 @@ "Please fix the highlighted fields before saving": "保存する前に強調表示された項目を修正してください", "Please log in with the appropriate credentials": "適切な認証情報でログインしてください", "Please select a container": "コンテナを選択してください", + "Please select a key to disable": "無効化するキーを選択してください", "Please select a payment method": "お支払い方法を選択してください", "Please select a primary model": "プライマリモデルを選択してください", "Please select a subscription plan": "サブスクリプションプランを選択してください", @@ -3871,11 +3878,11 @@ "Shorten": "短縮", "Show": "表示", "Show All": "すべて表示", - "Show sensitive data": "機密データを表示", "Show all providers including unbound": "未バインドを含むすべてのプロバイダーを表示", "Show only bound providers": "バインド済みのプロバイダーのみ表示", "Show or hide flow columns": "フロー列の表示・非表示", "Show prices in currency instead of quota.": "クォータではなく通貨で価格を表示。", + "Show sensitive data": "機密データを表示", "Show setup guide": "セットアップガイドを表示", "Show token usage statistics in the UI": "UIでトークン使用統計を表示", "Showcase core capabilities with demo credentials and limited access.": "デモ用の認証情報と制限付きアクセスでコア機能を紹介します。", @@ -4124,6 +4131,7 @@ "Test connectivity for:": "接続性をテスト:", "Test failed": "テストに失敗しました", "Test interval (minutes)": "テスト間隔(分)", + "Test Key": "テストキー", "Test Latency": "レイテンシをテスト", "Test Mode": "テストモード", "Test Model": "モデルをテスト", diff --git a/web/default/src/i18n/locales/ru.json b/web/default/src/i18n/locales/ru.json index 51edeac76d4..cdcabc280d2 100644 --- a/web/default/src/i18n/locales/ru.json +++ b/web/default/src/i18n/locales/ru.json @@ -475,6 +475,7 @@ "Auto group behavior": "Поведение группы auto", "Auto Group Chain": "Автоматическая цепочка групп", "Auto refresh": "Автообновление", + "Auto select": "Автоматический выбор", "Auto Sync Upstream Models": "Автоматическая синхронизация моделей провайдера", "Auto-disable rules": "Правила автоотключения", "Auto-disable status codes": "Коды автоотключения", @@ -742,6 +743,7 @@ "checkout.session.completed": "checkout.session.completed", "checkout.session.expired": "checkout.session.expired", "Chinese": "Китайский", + "Choose a specific enabled key for this test.": "Выберите конкретный включенный ключ для этого теста.", "Choose a username": "Выберите имя пользователя", "Choose an amount and payment method": "Выберите сумму и способ оплаты", "Choose between default expanded, compact icon-only, or full layout mode": "Выберите между развернутым по умолчанию, компактным (только иконки) или полным режимом макета", @@ -1284,6 +1286,7 @@ "Disable": "Отключить", "Disable 2FA": "Отключить 2FA", "Disable All": "Отключить все", + "Disable current key": "Отключить текущий ключ", "Disable on failure": "Отключить при сбое", "Disable selected channels": "Отключить выбранные каналы", "Disable selected models": "Отключить выбранные модели", @@ -1708,6 +1711,7 @@ "Failed to disable {{count}} model(s)": "Не удалось отключить {{count}} моделей", "Failed to disable 2FA": "Не удалось отключить 2FA", "Failed to disable channels": "Не удалось отключить каналы", + "Failed to disable key": "Не удалось отключить ключ", "Failed to disable model": "Не удалось отключить модель", "Failed to disable tag channels": "Не удалось отключить каналы тегов", "Failed to discover OIDC endpoints": "Не удалось обнаружить конечные точки OIDC", @@ -1728,6 +1732,7 @@ "Failed to fetch user information": "Не удалось получить данные пользователя", "Failed to fix abilities": "Не удалось исправить способности", "Failed to generate token": "Не удалось сгенерировать токен", + "Failed to get key status": "Не удалось получить статус ключа", "Failed to initialize OAuth": "Не удалось инициализировать OAuth", "Failed to initialize system": "Не удалось инициализировать систему", "Failed to load": "Не удалось загрузить", @@ -2255,6 +2260,7 @@ "Keep this above 1 minute to avoid heavy database load": "Держите это значение выше 1 минуты, чтобы избежать высокой нагрузки на базу данных", "Keep-alive Ping": "Пинг Keep-alive", "Key": "Ключ", + "Key disabled": "Ключ отключен", "Key Fingerprint": "Отпечаток ключа", "Key Sources": "Источники ключей", "Key Summary": "Сводка ключа", @@ -3160,6 +3166,7 @@ "Please fix the highlighted fields before saving": "Исправьте выделенные поля перед сохранением", "Please log in with the appropriate credentials": "Пожалуйста, войдите с соответствующими учетными данными", "Please select a container": "Пожалуйста, выберите контейнер", + "Please select a key to disable": "Выберите ключ для отключения", "Please select a payment method": "Пожалуйста, выберите способ оплаты", "Please select a primary model": "Пожалуйста, выберите основную модель", "Please select a subscription plan": "Пожалуйста, выберите план подписки", @@ -3871,11 +3878,11 @@ "Shorten": "Сократить", "Show": "Показать", "Show All": "Показать все", - "Show sensitive data": "Показать конфиденциальные данные", "Show all providers including unbound": "Показать всех провайдеров (включая непривязанные)", "Show only bound providers": "Показать только привязанных провайдеров", "Show or hide flow columns": "Показать или скрыть столбцы потока", "Show prices in currency instead of quota.": "Показывать цены в валюте вместо квоты.", + "Show sensitive data": "Показать конфиденциальные данные", "Show setup guide": "Показать руководство по настройке", "Show token usage statistics in the UI": "Показывать статистику использования токенов в пользовательском интерфейсе", "Showcase core capabilities with demo credentials and limited access.": "Демонстрация основных возможностей с демо-учётными данными и ограниченным доступом.", @@ -4124,6 +4131,7 @@ "Test connectivity for:": "Проверить подключение для:", "Test failed": "Тест не выполнен", "Test interval (minutes)": "Интервал проверки (минуты)", + "Test Key": "Ключ для теста", "Test Latency": "Проверить задержку", "Test Mode": "Тестовый режим", "Test Model": "Проверить модель", diff --git a/web/default/src/i18n/locales/vi.json b/web/default/src/i18n/locales/vi.json index 30ef42c7c56..c0b51fd19d8 100644 --- a/web/default/src/i18n/locales/vi.json +++ b/web/default/src/i18n/locales/vi.json @@ -475,6 +475,7 @@ "Auto group behavior": "Cách hoạt động của nhóm auto", "Auto Group Chain": "Chuỗi nhóm tự động", "Auto refresh": "Tự động làm mới", + "Auto select": "Tự động chọn", "Auto Sync Upstream Models": "Tự động đồng bộ mô hình nguồn", "Auto-disable rules": "Quy tắc tự động tắt", "Auto-disable status codes": "Mã trạng thái tự tắt", @@ -742,6 +743,7 @@ "checkout.session.completed": "thanh toán.phiên.hoàn thành", "checkout.session.expired": "Phiên thanh toán đã hết hạn.", "Chinese": "Tiếng Trung", + "Choose a specific enabled key for this test.": "Chọn một khóa đang bật cụ thể cho lần kiểm tra này.", "Choose a username": "Chọn tên người dùng", "Choose an amount and payment method": "Chọn số tiền và phương thức thanh toán", "Choose between default expanded, compact icon-only, or full layout mode": "Chọn giữa chế độ mở rộng mặc định, chế độ chỉ biểu tượng thu gọn, hoặc chế độ bố cục đầy đủ", @@ -1284,6 +1286,7 @@ "Disable": "Vô hiệu hóa", "Disable 2FA": "Tắt 2FA", "Disable All": "Vô hiệu hóa tất cả", + "Disable current key": "Tắt khóa hiện tại", "Disable on failure": "Vô hiệu hóa khi lỗi", "Disable selected channels": "Vô hiệu hóa các kênh đã chọn", "Disable selected models": "Vô hiệu hóa các mô hình đã chọn", @@ -1708,6 +1711,7 @@ "Failed to disable {{count}} model(s)": "Không thể tắt {{count}} mô hình", "Failed to disable 2FA": "Không thể vô hiệu hóa 2FA", "Failed to disable channels": "Không thể vô hiệu hóa các kênh", + "Failed to disable key": "Không thể tắt khóa", "Failed to disable model": "Không thể vô hiệu hóa mô hình", "Failed to disable tag channels": "Không thể vô hiệu hóa các kênh thẻ", "Failed to discover OIDC endpoints": "Khám phá điểm cuối OIDC thất bại", @@ -1728,6 +1732,7 @@ "Failed to fetch user information": "Không lấy được thông tin người dùng", "Failed to fix abilities": "Không thể khắc phục các khả năng", "Failed to generate token": "Không thể tạo token", + "Failed to get key status": "Không thể lấy trạng thái khóa", "Failed to initialize OAuth": "Không thể khởi tạo OAuth", "Failed to initialize system": "Không thể khởi tạo hệ thống", "Failed to load": "Tải thất bại", @@ -2255,6 +2260,7 @@ "Keep this above 1 minute to avoid heavy database load": "Giữ cái này trên 1 phút để tránh tải nặng cơ sở dữ liệu", "Keep-alive Ping": "Ping duy trì", "Key": "Khóa", + "Key disabled": "Đã tắt khóa", "Key Fingerprint": "Vân tay khóa", "Key Sources": "Nguồn khóa", "Key Summary": "Tóm tắt khóa", @@ -3160,6 +3166,7 @@ "Please fix the highlighted fields before saving": "Vui lòng sửa các trường được đánh dấu trước khi lưu", "Please log in with the appropriate credentials": "Vui lòng đăng nhập bằng thông tin xác thực phù hợp", "Please select a container": "Vui lòng chọn một container", + "Please select a key to disable": "Vui lòng chọn khóa cần tắt", "Please select a payment method": "Vui lòng chọn phương thức thanh toán", "Please select a primary model": "Vui lòng chọn một mô hình chính", "Please select a subscription plan": "Vui lòng chọn gói đăng ký", @@ -3871,11 +3878,11 @@ "Shorten": "Rút gọn", "Show": "Hiển thị", "Show All": "Hiển thị tất cả", - "Show sensitive data": "Hiển thị dữ liệu nhạy cảm", "Show all providers including unbound": "Hiển thị tất cả nhà cung cấp (bao gồm chưa liên kết)", "Show only bound providers": "Chỉ hiển thị nhà cung cấp đã liên kết", "Show or hide flow columns": "Hiện hoặc ẩn các cột luồng", "Show prices in currency instead of quota.": "Hiển thị giá bằng tiền tệ thay vì hạn ngạch.", + "Show sensitive data": "Hiển thị dữ liệu nhạy cảm", "Show setup guide": "Hiển thị hướng dẫn thiết lập", "Show token usage statistics in the UI": "Hiển thị thống kê sử dụng token trong giao diện người dùng", "Showcase core capabilities with demo credentials and limited access.": "Trình diễn các tính năng cốt lõi với thông tin đăng nhập demo và quyền truy cập hạn chế.", @@ -4124,6 +4131,7 @@ "Test connectivity for:": "Kiểm tra kết nối cho:", "Test failed": "Kiểm tra thất bại", "Test interval (minutes)": "Khoảng thời gian kiểm tra (phút)", + "Test Key": "Khóa kiểm tra", "Test Latency": "Kiểm tra độ trễ", "Test Mode": "Chế độ thử nghiệm", "Test Model": "Kiểm tra Mô hình", diff --git a/web/default/src/i18n/locales/zh.json b/web/default/src/i18n/locales/zh.json index 7cb767114ad..a0343388604 100644 --- a/web/default/src/i18n/locales/zh.json +++ b/web/default/src/i18n/locales/zh.json @@ -475,6 +475,7 @@ "Auto group behavior": "自动分组行为", "Auto Group Chain": "自动分组链", "Auto refresh": "自动刷新", + "Auto select": "自动选择", "Auto Sync Upstream Models": "自动同步上游模型", "Auto-disable rules": "自动禁用规则", "Auto-disable status codes": "自动禁用状态码", @@ -742,6 +743,7 @@ "checkout.session.completed": "checkout.session.completed", "checkout.session.expired": "checkout.session.expired", "Chinese": "中文", + "Choose a specific enabled key for this test.": "选择一个启用的指定密钥进行本次测试。", "Choose a username": "选择一个用户名", "Choose an amount and payment method": "选择金额和支付方式", "Choose between default expanded, compact icon-only, or full layout mode": "选择默认展开、紧凑图标模式或完整布局模式", @@ -1284,6 +1286,7 @@ "Disable": "禁用", "Disable 2FA": "禁用 2FA", "Disable All": "禁用全部", + "Disable current key": "禁用当前密钥", "Disable on failure": "失败时禁用", "Disable selected channels": "禁用选定的渠道", "Disable selected models": "禁用选定的模型", @@ -1708,6 +1711,7 @@ "Failed to disable {{count}} model(s)": "禁用 {{count}} 个模型失败", "Failed to disable 2FA": "禁用 2FA 失败", "Failed to disable channels": "禁用渠道失败", + "Failed to disable key": "禁用密钥失败", "Failed to disable model": "禁用模型失败", "Failed to disable tag channels": "禁用标签渠道失败", "Failed to discover OIDC endpoints": "发现 OIDC 端点失败", @@ -1728,6 +1732,7 @@ "Failed to fetch user information": "获取用户信息失败", "Failed to fix abilities": "修复能力失败", "Failed to generate token": "生成令牌失败", + "Failed to get key status": "获取密钥状态失败", "Failed to initialize OAuth": "初始化 OAuth 失败", "Failed to initialize system": "系统初始化失败", "Failed to load": "加载失败", @@ -2255,6 +2260,7 @@ "Keep this above 1 minute to avoid heavy database load": "保持此值大于 1 分钟以避免数据库负载过重", "Keep-alive Ping": "保持连接心跳", "Key": "密钥", + "Key disabled": "密钥已禁用", "Key Fingerprint": "Key 指纹", "Key Sources": "Key 来源", "Key Summary": "Key 摘要", @@ -3160,6 +3166,7 @@ "Please fix the highlighted fields before saving": "请先修复高亮字段后再保存", "Please log in with the appropriate credentials": "请使用适当的凭据登录", "Please select a container": "请选择一个容器", + "Please select a key to disable": "请先选择要禁用的密钥", "Please select a payment method": "请选择支付方式", "Please select a primary model": "请选择主模型", "Please select a subscription plan": "请选择订阅套餐", @@ -3871,11 +3878,11 @@ "Shorten": "缩词", "Show": "显示", "Show All": "显示全部", - "Show sensitive data": "显示敏感数据", "Show all providers including unbound": "显示所有提供商(包括未绑定)", "Show only bound providers": "仅显示已绑定的提供商", "Show or hide flow columns": "显示或隐藏分流列", "Show prices in currency instead of quota.": "以货币而非配额显示价格。", + "Show sensitive data": "显示敏感数据", "Show setup guide": "显示设置引导", "Show token usage statistics in the UI": "在用户界面中显示令牌使用统计信息", "Showcase core capabilities with demo credentials and limited access.": "使用演示凭据和有限访问权限展示核心功能。", @@ -4124,6 +4131,7 @@ "Test connectivity for:": "测试连接性:", "Test failed": "测试失败", "Test interval (minutes)": "测试间隔 (分钟)", + "Test Key": "测试密钥", "Test Latency": "测试延迟", "Test Mode": "测试模式", "Test Model": "测试模型",