feat: web search provider picker for Vertex models (Google vs Enterprise)

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.
This commit is contained in:
Bruno Deanoz 2026-06-23 01:23:03 +02:00
parent 4150936b18
commit fb6bc981d1
5 changed files with 62 additions and 0 deletions

View file

@ -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))
}
}
}
}
}
}
}
}
}

View file

@ -114,6 +114,7 @@ fun ChatScreen(
var activeMode by remember { mutableStateOf<Int?>(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<String?>(null) }
var streamJob by remember { mutableStateOf<Job?>(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(

View file

@ -37,6 +37,7 @@ import java.util.concurrent.TimeUnit
val preferThroughput: Boolean? = null,
val attachments: List<Attachment>? = 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<Attachment>? = 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,

View file

@ -48,6 +48,8 @@
<string name="mode_search">Search</string>
<string name="mode_video">Video</string>
<string name="mode_audio">Audio</string>
<string name="search_provider_google">Google</string>
<string name="search_provider_enterprise">Enterprise</string>
<!-- Suggestion pills -->
<string name="suggest_brainstorm">Brainstorm</string>

View file

@ -50,6 +50,8 @@
<string name="mode_search">Suche</string>
<string name="mode_video">Video</string>
<string name="mode_audio">Audio</string>
<string name="search_provider_google">Google</string>
<string name="search_provider_enterprise">Enterprise</string>
<!-- Suggestion pills -->
<string name="suggest_brainstorm">Brainstormen</string>