From fb6bc981d133527cf9265f92d1453e8a35d092bf Mon Sep 17 00:00:00 2001 From: Bruno Deanoz Date: Tue, 23 Jun 2026 01:23:03 +0200 Subject: [PATCH] feat: web search provider picker for Vertex models (Google vs Enterprise) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When search mode is active and a vertex: model is selected, a second pill appears next to "Suche" with a dropdown to choose between Google Search (default) and Enterprise Web Search grounding. The selection is sent as webSearchProvider in the chat request body. Matches the web frontend's OptionPill for webSearchProvider. Only visible for Vertex models — OpenRouter uses agentive web_search server-tool which has no provider toggle. --- .../dev/kaizen/app/chat/ChatComponents.kt | 50 +++++++++++++++++++ .../java/dev/kaizen/app/chat/ChatScreen.kt | 5 ++ .../main/java/dev/kaizen/app/net/KaizenApi.kt | 3 ++ app/src/main/res/values-en/strings.xml | 2 + app/src/main/res/values/strings.xml | 2 + 5 files changed, 62 insertions(+) diff --git a/app/src/main/java/dev/kaizen/app/chat/ChatComponents.kt b/app/src/main/java/dev/kaizen/app/chat/ChatComponents.kt index 6db2d06..45eae75 100644 --- a/app/src/main/java/dev/kaizen/app/chat/ChatComponents.kt +++ b/app/src/main/java/dev/kaizen/app/chat/ChatComponents.kt @@ -151,6 +151,9 @@ fun ModePillsRow( onReasoningChange: (ReasoningPreset) -> Unit, samplingPrefs: SamplingPrefs, onSamplingChange: (SamplingPrefs) -> Unit, + currentModelId: String = "", + webSearchProvider: String = "google", + onWebSearchProviderChange: (String) -> Unit = {}, modifier: Modifier = Modifier, ) { val cs = MaterialTheme.colorScheme @@ -159,6 +162,8 @@ fun ModePillsRow( var showReasoningMenu by remember { mutableStateOf(false) } var showSamplingPopup by remember { mutableStateOf(false) } + var showSearchProviderMenu by remember { mutableStateOf(false) } + val isVertexModel = currentModelId.startsWith("vertex:") Row( modifier @@ -255,6 +260,51 @@ fun ModePillsRow( active = active, onClick = { onToggle(mode.labelRes) }, ) + + // Search provider dropdown — only for Vertex models when search is active + if (mode.labelRes == R.string.mode_search && active && isVertexModel) { + Box { + val providerLabel = stringResource( + if (webSearchProvider == "enterprise") R.string.search_provider_enterprise + else R.string.search_provider_google + ) + ModePill( + icon = KaizenIcons.Globe, + label = providerLabel, + active = true, + showChevron = true, + onClick = { showSearchProviderMenu = true }, + ) + DropdownMenu( + expanded = showSearchProviderMenu, + onDismissRequest = { showSearchProviderMenu = false }, + containerColor = if (isDark) Color(0xFF1A2030) else Color(0xFFF8F9FB), + shadowElevation = 16.dp, + shape = KaizenShapes.md, + ) { + listOf("google" to R.string.search_provider_google, "enterprise" to R.string.search_provider_enterprise).forEach { (value, labelRes) -> + Row( + Modifier + .fillMaxWidth() + .clickable { onWebSearchProviderChange(value); showSearchProviderMenu = false } + .padding(horizontal = 16.dp, vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + stringResource(labelRes), + fontSize = 13.5.sp, + color = if (value == webSearchProvider) accent.primary else cs.onBackground.copy(alpha = 0.85f), + fontWeight = if (value == webSearchProvider) FontWeight.SemiBold else FontWeight.Normal, + modifier = Modifier.weight(1f), + ) + if (value == webSearchProvider) { + Icon(KaizenIcons.Check, null, tint = accent.primary, modifier = Modifier.size(15.dp)) + } + } + } + } + } + } } } } diff --git a/app/src/main/java/dev/kaizen/app/chat/ChatScreen.kt b/app/src/main/java/dev/kaizen/app/chat/ChatScreen.kt index f01286a..142db93 100644 --- a/app/src/main/java/dev/kaizen/app/chat/ChatScreen.kt +++ b/app/src/main/java/dev/kaizen/app/chat/ChatScreen.kt @@ -114,6 +114,7 @@ fun ChatScreen( var activeMode by remember { mutableStateOf(null) } var reasoningPreset by remember { mutableStateOf(ReasoningPreset.Standard) } var samplingPrefs by remember { mutableStateOf(SamplingPrefs()) } + var webSearchProvider by remember { mutableStateOf("google") } var usedTokens by remember { mutableStateOf(0) } var chatModel by remember { mutableStateOf(null) } var streamJob by remember { mutableStateOf(null) } @@ -584,6 +585,7 @@ fun ChatScreen( preferThroughput = reasoningPreset.throughput, attachments = uploadedAtts.takeIf { it.isNotEmpty() }, webSearch = activeMode == R.string.mode_search, + webSearchProvider = if (activeMode == R.string.mode_search && cfg.model.startsWith("vertex:")) webSearchProvider else null, temperature = samplingPrefs.temperature.takeIf { it.enabled }?.value, topP = samplingPrefs.topP.takeIf { it.enabled }?.value, topK = samplingPrefs.topK.takeIf { it.enabled }?.value?.toInt(), @@ -1055,6 +1057,9 @@ fun ChatScreen( onReasoningChange = { reasoningPreset = it; haptics.tick() }, samplingPrefs = samplingPrefs, onSamplingChange = { samplingPrefs = it }, + currentModelId = session.config?.model ?: "", + webSearchProvider = webSearchProvider, + onWebSearchProviderChange = { webSearchProvider = it; haptics.tick() }, ) Spacer(Modifier.height(if (imeVisible) 6.dp else 10.dp)) ChatInput( diff --git a/app/src/main/java/dev/kaizen/app/net/KaizenApi.kt b/app/src/main/java/dev/kaizen/app/net/KaizenApi.kt index b47e745..0c1b3da 100644 --- a/app/src/main/java/dev/kaizen/app/net/KaizenApi.kt +++ b/app/src/main/java/dev/kaizen/app/net/KaizenApi.kt @@ -37,6 +37,7 @@ import java.util.concurrent.TimeUnit val preferThroughput: Boolean? = null, val attachments: List? = null, val webSearch: Boolean? = null, + val webSearchProvider: String? = null, val temperature: Float? = null, val topP: Float? = null, val topK: Int? = null, @@ -487,6 +488,7 @@ object KaizenApi { preferThroughput: Boolean = false, attachments: List? = null, webSearch: Boolean = false, + webSearchProvider: String? = null, temperature: Float? = null, topP: Float? = null, topK: Int? = null, @@ -499,6 +501,7 @@ object KaizenApi { preferThroughput = if (preferThroughput) true else null, attachments = attachments?.takeIf { it.isNotEmpty() }, webSearch = if (webSearch) true else null, + webSearchProvider = if (webSearch) webSearchProvider else null, temperature = temperature, topP = topP, topK = topK, diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml index b28c744..40121ea 100644 --- a/app/src/main/res/values-en/strings.xml +++ b/app/src/main/res/values-en/strings.xml @@ -48,6 +48,8 @@ Search Video Audio + Google + Enterprise Brainstorm diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5301c71..5b2d3d4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -50,6 +50,8 @@ Suche Video Audio + Google + Enterprise Brainstormen