feat: gate model overrides and load full token history (#271)

Co-authored-by: zuolingxuan <zuolingxuan@bytedance.com>
This commit is contained in:
Lingxuan Zuo
2026-03-04 11:52:54 +08:00
committed by GitHub
Unverified
parent 3371e4fe74
commit 30b03add1c
12 changed files with 181 additions and 33 deletions

View File

@@ -30,11 +30,14 @@ import {
type ProviderType,
getProviderIconUrl,
resolveProviderApiKeyForSave,
resolveProviderModelForSave,
shouldShowProviderModelId,
shouldInvertInDark,
} from '@/lib/providers';
import { cn } from '@/lib/utils';
import { toast } from 'sonner';
import { useTranslation } from 'react-i18next';
import { useSettingsStore } from '@/stores/settings';
function normalizeFallbackProviderIds(ids?: string[]): string[] {
return Array.from(new Set((ids ?? []).filter(Boolean)));
@@ -58,6 +61,7 @@ function fallbackModelsEqual(a?: string[], b?: string[]): boolean {
export function ProvidersSettings() {
const { t } = useTranslation('settings');
const devModeUnlocked = useSettingsStore((state) => state.devModeUnlocked);
const {
providers,
defaultProviderId,
@@ -180,6 +184,7 @@ export function ProvidersSettings() {
setEditingProvider(null);
}}
onValidateKey={(key, options) => validateApiKey(provider.id, key, options)}
devModeUnlocked={devModeUnlocked}
/>
))}
</div>
@@ -192,6 +197,7 @@ export function ProvidersSettings() {
onClose={() => setShowAddDialog(false)}
onAdd={handleAddProvider}
onValidateKey={(type, key, options) => validateApiKey(type, key, options)}
devModeUnlocked={devModeUnlocked}
/>
)}
</div>
@@ -212,6 +218,7 @@ interface ProviderCardProps {
key: string,
options?: { baseUrl?: string }
) => Promise<{ valid: boolean; error?: string }>;
devModeUnlocked: boolean;
}
@@ -227,6 +234,7 @@ function ProviderCard({
onSetDefault,
onSaveEdits,
onValidateKey,
devModeUnlocked,
}: ProviderCardProps) {
const { t } = useTranslation('settings');
const [newKey, setNewKey] = useState('');
@@ -243,7 +251,8 @@ function ProviderCard({
const [saving, setSaving] = useState(false);
const typeInfo = PROVIDER_TYPE_INFO.find((t) => t.id === provider.type);
const canEditModelConfig = Boolean(typeInfo?.showBaseUrl || typeInfo?.showModelId);
const showModelIdField = shouldShowProviderModelId(typeInfo, devModeUnlocked);
const canEditModelConfig = Boolean(typeInfo?.showBaseUrl || showModelIdField);
useEffect(() => {
if (isEditing) {
@@ -287,7 +296,7 @@ function ProviderCard({
}
{
if (typeInfo?.showModelId && !modelId.trim()) {
if (showModelIdField && !modelId.trim()) {
toast.error(t('aiProviders.toast.modelRequired'));
setSaving(false);
return;
@@ -297,7 +306,7 @@ function ProviderCard({
if (typeInfo?.showBaseUrl && (baseUrl.trim() || undefined) !== (provider.baseUrl || undefined)) {
updates.baseUrl = baseUrl.trim() || undefined;
}
if (typeInfo?.showModelId && (modelId.trim() || undefined) !== (provider.model || undefined)) {
if (showModelIdField && (modelId.trim() || undefined) !== (provider.model || undefined)) {
updates.model = modelId.trim() || undefined;
}
if (!fallbackModelsEqual(normalizedFallbackModels, provider.fallbackModels)) {
@@ -371,13 +380,13 @@ function ProviderCard({
/>
</div>
)}
{typeInfo?.showModelId && (
{showModelIdField && (
<div className="space-y-1">
<Label className="text-xs">{t('aiProviders.dialog.modelId')}</Label>
<Input
value={modelId}
onChange={(e) => setModelId(e.target.value)}
placeholder={typeInfo.modelIdPlaceholder || 'provider/model-id'}
placeholder={typeInfo?.modelIdPlaceholder || 'provider/model-id'}
className="h-9 text-sm"
/>
</div>
@@ -479,7 +488,7 @@ function ProviderCard({
&& fallbackModelsEqual(normalizeFallbackModels(fallbackModelsText.split('\n')), provider.fallbackModels)
&& fallbackProviderIdsEqual(fallbackProviderIds, provider.fallbackProviderIds)
)
|| Boolean(typeInfo?.showModelId && !modelId.trim())
|| Boolean(showModelIdField && !modelId.trim())
}
>
{validating || saving ? (
@@ -581,9 +590,16 @@ interface AddProviderDialogProps {
apiKey: string,
options?: { baseUrl?: string }
) => Promise<{ valid: boolean; error?: string }>;
devModeUnlocked: boolean;
}
function AddProviderDialog({ existingTypes, onClose, onAdd, onValidateKey }: AddProviderDialogProps) {
function AddProviderDialog({
existingTypes,
onClose,
onAdd,
onValidateKey,
devModeUnlocked,
}: AddProviderDialogProps) {
const { t } = useTranslation('settings');
const [selectedType, setSelectedType] = useState<ProviderType | null>(null);
const [name, setName] = useState('');
@@ -606,6 +622,7 @@ function AddProviderDialog({ existingTypes, onClose, onAdd, onValidateKey }: Add
const [authMode, setAuthMode] = useState<'oauth' | 'apikey'>('oauth');
const typeInfo = PROVIDER_TYPE_INFO.find((t) => t.id === selectedType);
const showModelIdField = shouldShowProviderModelId(typeInfo, devModeUnlocked);
const isOAuth = typeInfo?.isOAuth ?? false;
const supportsApiKey = typeInfo?.supportsApiKey ?? false;
// Effective OAuth mode: pure OAuth providers, or dual-mode with oauth selected
@@ -740,7 +757,7 @@ function AddProviderDialog({ existingTypes, onClose, onAdd, onValidateKey }: Add
}
}
const requiresModel = typeInfo?.showModelId ?? false;
const requiresModel = showModelIdField;
if (requiresModel && !modelId.trim()) {
setValidationError(t('aiProviders.toast.modelRequired'));
setSaving(false);
@@ -753,7 +770,7 @@ function AddProviderDialog({ existingTypes, onClose, onAdd, onValidateKey }: Add
apiKey.trim(),
{
baseUrl: baseUrl.trim() || undefined,
model: (typeInfo?.defaultModelId || modelId.trim()) || undefined,
model: resolveProviderModelForSave(typeInfo, modelId, devModeUnlocked),
}
);
} catch {
@@ -911,12 +928,12 @@ function AddProviderDialog({ existingTypes, onClose, onAdd, onValidateKey }: Add
</div>
)}
{typeInfo?.showModelId && (
{showModelIdField && (
<div className="space-y-2">
<Label htmlFor="modelId">{t('aiProviders.dialog.modelId')}</Label>
<Input
id="modelId"
placeholder={typeInfo.modelIdPlaceholder || 'provider/model-id'}
placeholder={typeInfo?.modelIdPlaceholder || 'provider/model-id'}
value={modelId}
onChange={(e) => {
setModelId(e.target.value);
@@ -1029,7 +1046,7 @@ function AddProviderDialog({ existingTypes, onClose, onAdd, onValidateKey }: Add
<Button
onClick={handleAdd}
className={cn(useOAuthFlow && "hidden")}
disabled={!selectedType || saving || ((typeInfo?.showModelId ?? false) && modelId.trim().length === 0)}
disabled={!selectedType || saving || (showModelIdField && modelId.trim().length === 0)}
>
{saving ? (
<Loader2 className="h-4 w-4 animate-spin mr-2" />