diff --git a/electron/main/ipc-handlers.ts b/electron/main/ipc-handlers.ts index aa2ee9810..8109e23e5 100644 --- a/electron/main/ipc-handlers.ts +++ b/electron/main/ipc-handlers.ts @@ -1882,8 +1882,8 @@ function registerSettingsHandlers(gatewayManager: GatewayManager): void { function registerUsageHandlers(): void { ipcMain.handle('usage:recentTokenHistory', async (_, limit?: number) => { const safeLimit = typeof limit === 'number' && Number.isFinite(limit) - ? Math.min(Math.max(Math.floor(limit), 1), 100) - : 20; + ? Math.max(Math.floor(limit), 1) + : undefined; return await getRecentTokenUsageHistory(safeLimit); }); } diff --git a/electron/utils/token-usage-core.ts b/electron/utils/token-usage-core.ts index ce25aaba2..275c3d6a0 100644 --- a/electron/utils/token-usage-core.ts +++ b/electron/utils/token-usage-core.ts @@ -41,12 +41,15 @@ interface TranscriptLineShape { export function parseUsageEntriesFromJsonl( content: string, context: { sessionId: string; agentId: string }, - limit = 20, + limit?: number, ): TokenUsageHistoryEntry[] { const entries: TokenUsageHistoryEntry[] = []; const lines = content.split(/\r?\n/).filter(Boolean); + const maxEntries = typeof limit === 'number' && Number.isFinite(limit) + ? Math.max(Math.floor(limit), 0) + : Number.POSITIVE_INFINITY; - for (let i = lines.length - 1; i >= 0 && entries.length < limit; i -= 1) { + for (let i = lines.length - 1; i >= 0 && entries.length < maxEntries; i -= 1) { let parsed: TranscriptLineShape; try { parsed = JSON.parse(lines[i]) as TranscriptLineShape; diff --git a/electron/utils/token-usage.ts b/electron/utils/token-usage.ts index eb61e34ff..9a29132f7 100644 --- a/electron/utils/token-usage.ts +++ b/electron/utils/token-usage.ts @@ -46,18 +46,21 @@ async function listRecentSessionFiles(): Promise { +export async function getRecentTokenUsageHistory(limit?: number): Promise { const files = await listRecentSessionFiles(); const results: TokenUsageHistoryEntry[] = []; + const maxEntries = typeof limit === 'number' && Number.isFinite(limit) + ? Math.max(Math.floor(limit), 0) + : Number.POSITIVE_INFINITY; for (const file of files) { - if (results.length >= limit) break; + if (results.length >= maxEntries) break; try { const content = await readFile(file.filePath, 'utf8'); const entries = parseUsageEntriesFromJsonl(content, { sessionId: file.sessionId, agentId: file.agentId, - }, limit - results.length); + }, Number.isFinite(maxEntries) ? maxEntries - results.length : undefined); results.push(...entries); } catch (error) { logger.debug(`Failed to read token usage transcript ${file.filePath}:`, error); @@ -65,5 +68,5 @@ export async function getRecentTokenUsageHistory(limit = 20): Promise Date.parse(b.timestamp) - Date.parse(a.timestamp)); - return results.slice(0, limit); + return Number.isFinite(maxEntries) ? results.slice(0, maxEntries) : results; } diff --git a/src/components/settings/ProvidersSettings.tsx b/src/components/settings/ProvidersSettings.tsx index 5bcc3613f..edb599c7a 100644 --- a/src/components/settings/ProvidersSettings.tsx +++ b/src/components/settings/ProvidersSettings.tsx @@ -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} /> ))} @@ -192,6 +197,7 @@ export function ProvidersSettings() { onClose={() => setShowAddDialog(false)} onAdd={handleAddProvider} onValidateKey={(type, key, options) => validateApiKey(type, key, options)} + devModeUnlocked={devModeUnlocked} /> )} @@ -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({ /> )} - {typeInfo?.showModelId && ( + {showModelIdField && (
setModelId(e.target.value)} - placeholder={typeInfo.modelIdPlaceholder || 'provider/model-id'} + placeholder={typeInfo?.modelIdPlaceholder || 'provider/model-id'} className="h-9 text-sm" />
@@ -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(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 )} - {typeInfo?.showModelId && ( + {showModelIdField && (
{ setModelId(e.target.value); @@ -1029,7 +1046,7 @@ function AddProviderDialog({ existingTypes, onClose, onAdd, onValidateKey }: Add