feat: gate model overrides and load full token history (#271)
Co-authored-by: zuolingxuan <zuolingxuan@bytedance.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
3371e4fe74
commit
30b03add1c
@@ -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" />
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
"last7Days": "7 days",
|
||||
"last30Days": "30 days",
|
||||
"allTime": "All",
|
||||
"showingLast": "Showing the latest {{count}} records",
|
||||
"showingLast": "{{count}} records",
|
||||
"totalTokens": "total tokens",
|
||||
"inputShort": "Input",
|
||||
"outputShort": "Output",
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
"last7Days": "7日",
|
||||
"last30Days": "30日",
|
||||
"allTime": "すべて",
|
||||
"showingLast": "最新 {{count}} 件を表示",
|
||||
"showingLast": "{{count}} 件",
|
||||
"totalTokens": "合計トークン",
|
||||
"inputShort": "入力",
|
||||
"outputShort": "出力",
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
"last7Days": "7 天",
|
||||
"last30Days": "30 天",
|
||||
"allTime": "全部",
|
||||
"showingLast": "显示最近 {{count}} 条记录",
|
||||
"showingLast": "共 {{count}} 条记录",
|
||||
"totalTokens": "总 token",
|
||||
"inputShort": "输入",
|
||||
"outputShort": "输出",
|
||||
|
||||
@@ -55,6 +55,8 @@ export interface ProviderTypeInfo {
|
||||
showBaseUrl?: boolean;
|
||||
/** Whether to show a Model ID input field (for providers where user picks the model) */
|
||||
showModelId?: boolean;
|
||||
/** Whether the Model ID input should only be shown in developer mode */
|
||||
showModelIdInDevModeOnly?: boolean;
|
||||
/** Default / example model ID placeholder */
|
||||
modelIdPlaceholder?: string;
|
||||
/** Default model ID to pre-fill */
|
||||
@@ -74,10 +76,10 @@ export const PROVIDER_TYPE_INFO: ProviderTypeInfo[] = [
|
||||
{ id: 'anthropic', name: 'Anthropic', icon: '🤖', placeholder: 'sk-ant-api03-...', model: 'Claude', requiresApiKey: true },
|
||||
{ id: 'openai', name: 'OpenAI', icon: '💚', placeholder: 'sk-proj-...', model: 'GPT', requiresApiKey: true },
|
||||
{ id: 'google', name: 'Google', icon: '🔷', placeholder: 'AIza...', model: 'Gemini', requiresApiKey: true },
|
||||
{ id: 'openrouter', name: 'OpenRouter', icon: '🌐', placeholder: 'sk-or-v1-...', model: 'Multi-Model', requiresApiKey: true },
|
||||
{ id: 'openrouter', name: 'OpenRouter', icon: '🌐', placeholder: 'sk-or-v1-...', model: 'Multi-Model', requiresApiKey: true, showModelId: true, showModelIdInDevModeOnly: true, modelIdPlaceholder: 'anthropic/claude-opus-4.6', defaultModelId: 'anthropic/claude-opus-4.6' },
|
||||
{ id: 'ark', name: 'ByteDance Ark', icon: 'A', placeholder: 'your-ark-api-key', model: 'Doubao', requiresApiKey: true, defaultBaseUrl: 'https://ark.cn-beijing.volces.com/api/v3', showBaseUrl: true, showModelId: true, modelIdPlaceholder: 'ep-20260228000000-xxxxx' },
|
||||
{ id: 'moonshot', name: 'Moonshot (CN)', icon: '🌙', placeholder: 'sk-...', model: 'Kimi', requiresApiKey: true, defaultBaseUrl: 'https://api.moonshot.cn/v1', defaultModelId: 'kimi-k2.5' },
|
||||
{ id: 'siliconflow', name: 'SiliconFlow (CN)', icon: '🌊', placeholder: 'sk-...', model: 'Multi-Model', requiresApiKey: true, defaultBaseUrl: 'https://api.siliconflow.cn/v1', defaultModelId: 'Pro/moonshotai/Kimi-K2.5' },
|
||||
{ id: 'siliconflow', name: 'SiliconFlow (CN)', icon: '🌊', placeholder: 'sk-...', model: 'Multi-Model', requiresApiKey: true, defaultBaseUrl: 'https://api.siliconflow.cn/v1', showModelId: true, showModelIdInDevModeOnly: true, modelIdPlaceholder: 'deepseek-ai/DeepSeek-V3', defaultModelId: 'deepseek-ai/DeepSeek-V3' },
|
||||
{ id: 'minimax-portal', name: 'MiniMax (Global)', icon: '☁️', placeholder: 'sk-...', model: 'MiniMax', requiresApiKey: false, isOAuth: true, supportsApiKey: true, defaultModelId: 'MiniMax-M2.5', apiKeyUrl: 'https://intl.minimaxi.com/' },
|
||||
{ id: 'minimax-portal-cn', name: 'MiniMax (CN)', icon: '☁️', placeholder: 'sk-...', model: 'MiniMax', requiresApiKey: false, isOAuth: true, supportsApiKey: true, defaultModelId: 'MiniMax-M2.5', apiKeyUrl: 'https://platform.minimaxi.com/' },
|
||||
{ id: 'qwen-portal', name: 'Qwen', icon: '☁️', placeholder: 'sk-...', model: 'Qwen', requiresApiKey: false, isOAuth: true, defaultModelId: 'coder-model' },
|
||||
@@ -103,6 +105,28 @@ export function getProviderTypeInfo(type: ProviderType): ProviderTypeInfo | unde
|
||||
return PROVIDER_TYPE_INFO.find((t) => t.id === type);
|
||||
}
|
||||
|
||||
export function shouldShowProviderModelId(
|
||||
provider: Pick<ProviderTypeInfo, 'showModelId' | 'showModelIdInDevModeOnly'> | undefined,
|
||||
devModeUnlocked: boolean
|
||||
): boolean {
|
||||
if (!provider?.showModelId) return false;
|
||||
if (provider.showModelIdInDevModeOnly && !devModeUnlocked) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
export function resolveProviderModelForSave(
|
||||
provider: Pick<ProviderTypeInfo, 'defaultModelId' | 'showModelId' | 'showModelIdInDevModeOnly'> | undefined,
|
||||
modelId: string,
|
||||
devModeUnlocked: boolean
|
||||
): string | undefined {
|
||||
if (!shouldShowProviderModelId(provider, devModeUnlocked)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const trimmedModelId = modelId.trim();
|
||||
return trimmedModelId || provider?.defaultModelId || undefined;
|
||||
}
|
||||
|
||||
/** Normalize provider API key before saving; Ollama uses a local placeholder when blank. */
|
||||
export function resolveProviderApiKeyForSave(type: ProviderType | string, apiKey: string): string | undefined {
|
||||
const trimmed = apiKey.trim();
|
||||
|
||||
@@ -63,7 +63,7 @@ export function Dashboard() {
|
||||
if (isGatewayRunning) {
|
||||
fetchChannels();
|
||||
fetchSkills();
|
||||
window.electron.ipcRenderer.invoke('usage:recentTokenHistory', 60)
|
||||
window.electron.ipcRenderer.invoke('usage:recentTokenHistory')
|
||||
.then((entries) => {
|
||||
setUsageHistory(Array.isArray(entries) ? entries as typeof usageHistory : []);
|
||||
setUsagePage(1);
|
||||
|
||||
@@ -103,7 +103,15 @@ const defaultSkills: DefaultSkill[] = [
|
||||
{ id: 'terminal', name: 'Terminal', description: 'Shell command execution' },
|
||||
];
|
||||
|
||||
import { SETUP_PROVIDERS, type ProviderTypeInfo, getProviderIconUrl, resolveProviderApiKeyForSave, shouldInvertInDark } from '@/lib/providers';
|
||||
import {
|
||||
SETUP_PROVIDERS,
|
||||
type ProviderTypeInfo,
|
||||
getProviderIconUrl,
|
||||
resolveProviderApiKeyForSave,
|
||||
resolveProviderModelForSave,
|
||||
shouldInvertInDark,
|
||||
shouldShowProviderModelId,
|
||||
} from '@/lib/providers';
|
||||
import clawxIcon from '@/assets/logo.svg';
|
||||
|
||||
// Use the shared provider registry for setup providers
|
||||
@@ -707,6 +715,7 @@ function ProviderContent({
|
||||
onConfiguredChange,
|
||||
}: ProviderContentProps) {
|
||||
const { t } = useTranslation(['setup', 'settings']);
|
||||
const devModeUnlocked = useSettingsStore((state) => state.devModeUnlocked);
|
||||
const [showKey, setShowKey] = useState(false);
|
||||
const [validating, setValidating] = useState(false);
|
||||
const [keyValid, setKeyValid] = useState<boolean | null>(null);
|
||||
@@ -910,7 +919,7 @@ function ProviderContent({
|
||||
? getProviderIconUrl(selectedProviderData.id)
|
||||
: undefined;
|
||||
const showBaseUrlField = selectedProviderData?.showBaseUrl ?? false;
|
||||
const showModelIdField = selectedProviderData?.showModelId ?? false;
|
||||
const showModelIdField = shouldShowProviderModelId(selectedProviderData, devModeUnlocked);
|
||||
const requiresKey = selectedProviderData?.requiresApiKey ?? false;
|
||||
const isOAuth = selectedProviderData?.isOAuth ?? false;
|
||||
const supportsApiKey = selectedProviderData?.supportsApiKey ?? false;
|
||||
@@ -958,10 +967,11 @@ function ProviderContent({
|
||||
setKeyValid(true);
|
||||
}
|
||||
|
||||
const effectiveModelId =
|
||||
selectedProviderData?.defaultModelId ||
|
||||
modelId.trim() ||
|
||||
undefined;
|
||||
const effectiveModelId = resolveProviderModelForSave(
|
||||
selectedProviderData,
|
||||
modelId,
|
||||
devModeUnlocked
|
||||
);
|
||||
|
||||
const providerIdForSave =
|
||||
selectedProvider === 'custom'
|
||||
|
||||
Reference in New Issue
Block a user