Browse Source

Merge pull request #7041 from antpyykk-kone/feature/configure-private-api-key-usage

feat: Ability to configure the use of private API keys in an environment
Timothy Jaeryang Baek 5 months ago
parent
commit
0cbb4572f6

+ 3 - 0
backend/open_webui/apps/webui/main.py

@@ -35,6 +35,7 @@ from open_webui.config import (
     ENABLE_LOGIN_FORM,
     ENABLE_LOGIN_FORM,
     ENABLE_MESSAGE_RATING,
     ENABLE_MESSAGE_RATING,
     ENABLE_SIGNUP,
     ENABLE_SIGNUP,
+    ENABLE_API_KEY,
     ENABLE_EVALUATION_ARENA_MODELS,
     ENABLE_EVALUATION_ARENA_MODELS,
     EVALUATION_ARENA_MODELS,
     EVALUATION_ARENA_MODELS,
     DEFAULT_ARENA_MODEL,
     DEFAULT_ARENA_MODEL,
@@ -98,6 +99,8 @@ app.state.config = AppConfig()
 
 
 app.state.config.ENABLE_SIGNUP = ENABLE_SIGNUP
 app.state.config.ENABLE_SIGNUP = ENABLE_SIGNUP
 app.state.config.ENABLE_LOGIN_FORM = ENABLE_LOGIN_FORM
 app.state.config.ENABLE_LOGIN_FORM = ENABLE_LOGIN_FORM
+app.state.config.ENABLE_API_KEY = ENABLE_API_KEY
+
 app.state.config.JWT_EXPIRES_IN = JWT_EXPIRES_IN
 app.state.config.JWT_EXPIRES_IN = JWT_EXPIRES_IN
 app.state.AUTH_TRUSTED_EMAIL_HEADER = WEBUI_AUTH_TRUSTED_EMAIL_HEADER
 app.state.AUTH_TRUSTED_EMAIL_HEADER = WEBUI_AUTH_TRUSTED_EMAIL_HEADER
 app.state.AUTH_TRUSTED_NAME_HEADER = WEBUI_AUTH_TRUSTED_NAME_HEADER
 app.state.AUTH_TRUSTED_NAME_HEADER = WEBUI_AUTH_TRUSTED_NAME_HEADER

+ 14 - 2
backend/open_webui/apps/webui/routers/auths.py

@@ -18,9 +18,10 @@ from open_webui.apps.webui.models.auths import (
     UserResponse,
     UserResponse,
 )
 )
 from open_webui.apps.webui.models.users import Users
 from open_webui.apps.webui.models.users import Users
-from open_webui.config import WEBUI_AUTH
+
 from open_webui.constants import ERROR_MESSAGES, WEBHOOK_MESSAGES
 from open_webui.constants import ERROR_MESSAGES, WEBHOOK_MESSAGES
 from open_webui.env import (
 from open_webui.env import (
+    WEBUI_AUTH,
     WEBUI_AUTH_TRUSTED_EMAIL_HEADER,
     WEBUI_AUTH_TRUSTED_EMAIL_HEADER,
     WEBUI_AUTH_TRUSTED_NAME_HEADER,
     WEBUI_AUTH_TRUSTED_NAME_HEADER,
     WEBUI_SESSION_COOKIE_SAME_SITE,
     WEBUI_SESSION_COOKIE_SAME_SITE,
@@ -580,6 +581,7 @@ async def get_admin_config(request: Request, user=Depends(get_admin_user)):
     return {
     return {
         "SHOW_ADMIN_DETAILS": request.app.state.config.SHOW_ADMIN_DETAILS,
         "SHOW_ADMIN_DETAILS": request.app.state.config.SHOW_ADMIN_DETAILS,
         "ENABLE_SIGNUP": request.app.state.config.ENABLE_SIGNUP,
         "ENABLE_SIGNUP": request.app.state.config.ENABLE_SIGNUP,
+        "ENABLE_API_KEY": request.app.state.config.ENABLE_API_KEY,
         "DEFAULT_USER_ROLE": request.app.state.config.DEFAULT_USER_ROLE,
         "DEFAULT_USER_ROLE": request.app.state.config.DEFAULT_USER_ROLE,
         "JWT_EXPIRES_IN": request.app.state.config.JWT_EXPIRES_IN,
         "JWT_EXPIRES_IN": request.app.state.config.JWT_EXPIRES_IN,
         "ENABLE_COMMUNITY_SHARING": request.app.state.config.ENABLE_COMMUNITY_SHARING,
         "ENABLE_COMMUNITY_SHARING": request.app.state.config.ENABLE_COMMUNITY_SHARING,
@@ -590,6 +592,7 @@ async def get_admin_config(request: Request, user=Depends(get_admin_user)):
 class AdminConfig(BaseModel):
 class AdminConfig(BaseModel):
     SHOW_ADMIN_DETAILS: bool
     SHOW_ADMIN_DETAILS: bool
     ENABLE_SIGNUP: bool
     ENABLE_SIGNUP: bool
+    ENABLE_API_KEY: bool
     DEFAULT_USER_ROLE: str
     DEFAULT_USER_ROLE: str
     JWT_EXPIRES_IN: str
     JWT_EXPIRES_IN: str
     ENABLE_COMMUNITY_SHARING: bool
     ENABLE_COMMUNITY_SHARING: bool
@@ -602,6 +605,7 @@ async def update_admin_config(
 ):
 ):
     request.app.state.config.SHOW_ADMIN_DETAILS = form_data.SHOW_ADMIN_DETAILS
     request.app.state.config.SHOW_ADMIN_DETAILS = form_data.SHOW_ADMIN_DETAILS
     request.app.state.config.ENABLE_SIGNUP = form_data.ENABLE_SIGNUP
     request.app.state.config.ENABLE_SIGNUP = form_data.ENABLE_SIGNUP
+    request.app.state.config.ENABLE_API_KEY = form_data.ENABLE_API_KEY
 
 
     if form_data.DEFAULT_USER_ROLE in ["pending", "user", "admin"]:
     if form_data.DEFAULT_USER_ROLE in ["pending", "user", "admin"]:
         request.app.state.config.DEFAULT_USER_ROLE = form_data.DEFAULT_USER_ROLE
         request.app.state.config.DEFAULT_USER_ROLE = form_data.DEFAULT_USER_ROLE
@@ -620,6 +624,7 @@ async def update_admin_config(
     return {
     return {
         "SHOW_ADMIN_DETAILS": request.app.state.config.SHOW_ADMIN_DETAILS,
         "SHOW_ADMIN_DETAILS": request.app.state.config.SHOW_ADMIN_DETAILS,
         "ENABLE_SIGNUP": request.app.state.config.ENABLE_SIGNUP,
         "ENABLE_SIGNUP": request.app.state.config.ENABLE_SIGNUP,
+        "ENABLE_API_KEY": request.app.state.config.ENABLE_API_KEY,
         "DEFAULT_USER_ROLE": request.app.state.config.DEFAULT_USER_ROLE,
         "DEFAULT_USER_ROLE": request.app.state.config.DEFAULT_USER_ROLE,
         "JWT_EXPIRES_IN": request.app.state.config.JWT_EXPIRES_IN,
         "JWT_EXPIRES_IN": request.app.state.config.JWT_EXPIRES_IN,
         "ENABLE_COMMUNITY_SHARING": request.app.state.config.ENABLE_COMMUNITY_SHARING,
         "ENABLE_COMMUNITY_SHARING": request.app.state.config.ENABLE_COMMUNITY_SHARING,
@@ -733,9 +738,16 @@ async def update_ldap_config(
 
 
 # create api key
 # create api key
 @router.post("/api_key", response_model=ApiKey)
 @router.post("/api_key", response_model=ApiKey)
-async def create_api_key_(user=Depends(get_current_user)):
+async def create_api_key(request: Request, user=Depends(get_current_user)):
+    if not request.app.config.state.ENABLE_API_KEY:
+        raise HTTPException(
+            status.HTTP_403_FORBIDDEN,
+            detail=ERROR_MESSAGES.API_KEY_CREATION_NOT_ALLOWED,
+        )
+
     api_key = create_api_key()
     api_key = create_api_key()
     success = Users.update_user_api_key_by_id(user.id, api_key)
     success = Users.update_user_api_key_by_id(user.id, api_key)
+
     if success:
     if success:
         return {
         return {
             "api_key": api_key,
             "api_key": api_key,

+ 7 - 0
backend/open_webui/config.py

@@ -265,6 +265,13 @@ class AppConfig:
 # WEBUI_AUTH (Required for security)
 # WEBUI_AUTH (Required for security)
 ####################################
 ####################################
 
 
+ENABLE_API_KEY = PersistentConfig(
+    "ENABLE_API_KEY",
+    "auth.api_key.enable",
+    os.environ.get("ENABLE_API_KEY", "True").lower() == "true",
+)
+
+
 JWT_EXPIRES_IN = PersistentConfig(
 JWT_EXPIRES_IN = PersistentConfig(
     "JWT_EXPIRES_IN", "auth.jwt_expiry", os.environ.get("JWT_EXPIRES_IN", "-1")
     "JWT_EXPIRES_IN", "auth.jwt_expiry", os.environ.get("JWT_EXPIRES_IN", "-1")
 )
 )

+ 2 - 0
backend/open_webui/constants.py

@@ -62,6 +62,7 @@ class ERROR_MESSAGES(str, Enum):
     NOT_FOUND = "We could not find what you're looking for :/"
     NOT_FOUND = "We could not find what you're looking for :/"
     USER_NOT_FOUND = "We could not find what you're looking for :/"
     USER_NOT_FOUND = "We could not find what you're looking for :/"
     API_KEY_NOT_FOUND = "Oops! It looks like there's a hiccup. The API key is missing. Please make sure to provide a valid API key to access this feature."
     API_KEY_NOT_FOUND = "Oops! It looks like there's a hiccup. The API key is missing. Please make sure to provide a valid API key to access this feature."
+    API_KEY_NOT_ALLOWED = "Use of API key is not enabled in the environment."
 
 
     MALICIOUS = "Unusual activities detected, please try again in a few minutes."
     MALICIOUS = "Unusual activities detected, please try again in a few minutes."
 
 
@@ -75,6 +76,7 @@ class ERROR_MESSAGES(str, Enum):
     OPENAI_NOT_FOUND = lambda name="": "OpenAI API was not found"
     OPENAI_NOT_FOUND = lambda name="": "OpenAI API was not found"
     OLLAMA_NOT_FOUND = "WebUI could not connect to Ollama"
     OLLAMA_NOT_FOUND = "WebUI could not connect to Ollama"
     CREATE_API_KEY_ERROR = "Oops! Something went wrong while creating your API key. Please try again later. If the issue persists, contact support for assistance."
     CREATE_API_KEY_ERROR = "Oops! Something went wrong while creating your API key. Please try again later. If the issue persists, contact support for assistance."
+    API_KEY_CREATION_NOT_ALLOWED = "API key creation is not allowed in the environment."
 
 
     EMPTY_CONTENT = "The content provided is empty. Please ensure that there is text or data present before proceeding."
     EMPTY_CONTENT = "The content provided is empty. Please ensure that there is text or data present before proceeding."
 
 

+ 2 - 0
backend/open_webui/main.py

@@ -940,6 +940,7 @@ async def commit_session_after_request(request: Request, call_next):
 @app.middleware("http")
 @app.middleware("http")
 async def check_url(request: Request, call_next):
 async def check_url(request: Request, call_next):
     start_time = int(time.time())
     start_time = int(time.time())
+    request.state.enable_api_key = webui_app.state.config.ENABLE_API_KEY
     response = await call_next(request)
     response = await call_next(request)
     process_time = int(time.time()) - start_time
     process_time = int(time.time()) - start_time
     response.headers["X-Process-Time"] = str(process_time)
     response.headers["X-Process-Time"] = str(process_time)
@@ -2427,6 +2428,7 @@ async def get_app_config(request: Request):
             "auth": WEBUI_AUTH,
             "auth": WEBUI_AUTH,
             "auth_trusted_header": bool(webui_app.state.AUTH_TRUSTED_EMAIL_HEADER),
             "auth_trusted_header": bool(webui_app.state.AUTH_TRUSTED_EMAIL_HEADER),
             "enable_ldap": webui_app.state.config.ENABLE_LDAP,
             "enable_ldap": webui_app.state.config.ENABLE_LDAP,
+            "enable_api_key": webui_app.state.config.ENABLE_API_KEY,
             "enable_signup": webui_app.state.config.ENABLE_SIGNUP,
             "enable_signup": webui_app.state.config.ENABLE_SIGNUP,
             "enable_login_form": webui_app.state.config.ENABLE_LOGIN_FORM,
             "enable_login_form": webui_app.state.config.ENABLE_LOGIN_FORM,
             **(
             **(

+ 4 - 3
backend/open_webui/utils/utils.py

@@ -5,13 +5,11 @@ import jwt
 from datetime import UTC, datetime, timedelta
 from datetime import UTC, datetime, timedelta
 from typing import Optional, Union, List, Dict
 from typing import Optional, Union, List, Dict
 
 
-
 from open_webui.apps.webui.models.users import Users
 from open_webui.apps.webui.models.users import Users
 
 
 from open_webui.constants import ERROR_MESSAGES
 from open_webui.constants import ERROR_MESSAGES
 from open_webui.env import WEBUI_SECRET_KEY
 from open_webui.env import WEBUI_SECRET_KEY
 
 
-
 from fastapi import Depends, HTTPException, Request, Response, status
 from fastapi import Depends, HTTPException, Request, Response, status
 from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
 from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
 from passlib.context import CryptContext
 from passlib.context import CryptContext
@@ -93,10 +91,13 @@ def get_current_user(
 
 
     # auth by api key
     # auth by api key
     if token.startswith("sk-"):
     if token.startswith("sk-"):
+        if not request.state.enable_api_key:
+            raise HTTPException(
+                status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.API_KEY_NOT_ALLOWED
+            )
         return get_current_user_by_api_key(token)
         return get_current_user_by_api_key(token)
 
 
     # auth by jwt token
     # auth by jwt token
-
     try:
     try:
         data = decode_token(token)
         data = decode_token(token)
     except Exception as e:
     except Exception as e:

+ 6 - 0
src/lib/components/admin/Settings/General.svelte

@@ -112,6 +112,12 @@
 					</div>
 					</div>
 				</div>
 				</div>
 
 
+				<div class="  flex w-full justify-between pr-2">
+					<div class=" self-center text-xs font-medium">{$i18n.t('Enable API Key Auth')}</div>
+
+					<Switch bind:state={adminConfig.ENABLE_API_KEY} />
+				</div>
+
 				<hr class=" border-gray-50 dark:border-gray-850 my-2" />
 				<hr class=" border-gray-50 dark:border-gray-850 my-2" />
 
 
 				<div class="my-3 flex w-full items-center justify-between pr-2">
 				<div class="my-3 flex w-full items-center justify-between pr-2">

+ 83 - 83
src/lib/components/chat/Settings/Account.svelte

@@ -2,7 +2,7 @@
 	import { toast } from 'svelte-sonner';
 	import { toast } from 'svelte-sonner';
 	import { onMount, getContext } from 'svelte';
 	import { onMount, getContext } from 'svelte';
 
 
-	import { user } from '$lib/stores';
+	import { user, config } from '$lib/stores';
 	import { updateUserProfile, createAPIKey, getAPIKey } from '$lib/apis/auths';
 	import { updateUserProfile, createAPIKey, getAPIKey } from '$lib/apis/auths';
 
 
 	import UpdatePassword from './Account/UpdatePassword.svelte';
 	import UpdatePassword from './Account/UpdatePassword.svelte';
@@ -26,7 +26,6 @@
 
 
 	let APIKey = '';
 	let APIKey = '';
 	let APIKeyCopied = false;
 	let APIKeyCopied = false;
-
 	let profileImageInputElement: HTMLInputElement;
 	let profileImageInputElement: HTMLInputElement;
 
 
 	const submitHandler = async () => {
 	const submitHandler = async () => {
@@ -301,96 +300,97 @@
 						</button>
 						</button>
 					</div>
 					</div>
 				</div>
 				</div>
-				<div class="justify-between w-full">
-					<div class="flex justify-between w-full">
-						<div class="self-center text-xs font-medium">{$i18n.t('API Key')}</div>
-					</div>
+				{#if $config?.features?.enable_api_key ?? true}
+					<div class="justify-between w-full">
+						<div class="flex justify-between w-full">
+							<div class="self-center text-xs font-medium">{$i18n.t('API Key')}</div>
+						</div>
+						<div class="flex mt-2">
+							{#if APIKey}
+								<SensitiveInput value={APIKey} readOnly={true} />
 
 
-					<div class="flex mt-2">
-						{#if APIKey}
-							<SensitiveInput value={APIKey} readOnly={true} />
-
-							<button
-								class="ml-1.5 px-1.5 py-1 dark:hover:bg-gray-850 transition rounded-lg"
-								on:click={() => {
-									copyToClipboard(APIKey);
-									APIKeyCopied = true;
-									setTimeout(() => {
-										APIKeyCopied = false;
-									}, 2000);
-								}}
-							>
-								{#if APIKeyCopied}
-									<svg
-										xmlns="http://www.w3.org/2000/svg"
-										viewBox="0 0 20 20"
-										fill="currentColor"
-										class="w-4 h-4"
-									>
-										<path
-											fill-rule="evenodd"
-											d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
-											clip-rule="evenodd"
-										/>
-									</svg>
-								{:else}
-									<svg
-										xmlns="http://www.w3.org/2000/svg"
-										viewBox="0 0 16 16"
-										fill="currentColor"
-										class="w-4 h-4"
-									>
-										<path
-											fill-rule="evenodd"
-											d="M11.986 3H12a2 2 0 0 1 2 2v6a2 2 0 0 1-1.5 1.937V7A2.5 2.5 0 0 0 10 4.5H4.063A2 2 0 0 1 6 3h.014A2.25 2.25 0 0 1 8.25 1h1.5a2.25 2.25 0 0 1 2.236 2ZM10.5 4v-.75a.75.75 0 0 0-.75-.75h-1.5a.75.75 0 0 0-.75.75V4h3Z"
-											clip-rule="evenodd"
-										/>
-										<path
-											fill-rule="evenodd"
-											d="M3 6a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h7a1 1 0 0 0 1-1V7a1 1 0 0 0-1-1H3Zm1.75 2.5a.75.75 0 0 0 0 1.5h3.5a.75.75 0 0 0 0-1.5h-3.5ZM4 11.75a.75.75 0 0 1 .75-.75h3.5a.75.75 0 0 1 0 1.5h-3.5a.75.75 0 0 1-.75-.75Z"
-											clip-rule="evenodd"
-										/>
-									</svg>
-								{/if}
-							</button>
+								<button
+									class="ml-1.5 px-1.5 py-1 dark:hover:bg-gray-850 transition rounded-lg"
+									on:click={() => {
+										copyToClipboard(APIKey);
+										APIKeyCopied = true;
+										setTimeout(() => {
+											APIKeyCopied = false;
+										}, 2000);
+									}}
+								>
+									{#if APIKeyCopied}
+										<svg
+											xmlns="http://www.w3.org/2000/svg"
+											viewBox="0 0 20 20"
+											fill="currentColor"
+											class="w-4 h-4"
+										>
+											<path
+												fill-rule="evenodd"
+												d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
+												clip-rule="evenodd"
+											/>
+										</svg>
+									{:else}
+										<svg
+											xmlns="http://www.w3.org/2000/svg"
+											viewBox="0 0 16 16"
+											fill="currentColor"
+											class="w-4 h-4"
+										>
+											<path
+												fill-rule="evenodd"
+												d="M11.986 3H12a2 2 0 0 1 2 2v6a2 2 0 0 1-1.5 1.937V7A2.5 2.5 0 0 0 10 4.5H4.063A2 2 0 0 1 6 3h.014A2.25 2.25 0 0 1 8.25 1h1.5a2.25 2.25 0 0 1 2.236 2ZM10.5 4v-.75a.75.75 0 0 0-.75-.75h-1.5a.75.75 0 0 0-.75.75V4h3Z"
+												clip-rule="evenodd"
+											/>
+											<path
+												fill-rule="evenodd"
+												d="M3 6a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h7a1 1 0 0 0 1-1V7a1 1 0 0 0-1-1H3Zm1.75 2.5a.75.75 0 0 0 0 1.5h3.5a.75.75 0 0 0 0-1.5h-3.5ZM4 11.75a.75.75 0 0 1 .75-.75h3.5a.75.75 0 0 1 0 1.5h-3.5a.75.75 0 0 1-.75-.75Z"
+												clip-rule="evenodd"
+											/>
+										</svg>
+									{/if}
+								</button>
 
 
-							<Tooltip content={$i18n.t('Create new key')}>
+								<Tooltip content={$i18n.t('Create new key')}>
+									<button
+										class=" px-1.5 py-1 dark:hover:bg-gray-850transition rounded-lg"
+										on:click={() => {
+											createAPIKeyHandler();
+										}}
+									>
+										<svg
+											xmlns="http://www.w3.org/2000/svg"
+											fill="none"
+											viewBox="0 0 24 24"
+											stroke-width="2"
+											stroke="currentColor"
+											class="size-4"
+										>
+											<path
+												stroke-linecap="round"
+												stroke-linejoin="round"
+												d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99"
+											/>
+										</svg>
+									</button>
+								</Tooltip>
+							{:else}
 								<button
 								<button
-									class=" px-1.5 py-1 dark:hover:bg-gray-850transition rounded-lg"
+									class="flex gap-1.5 items-center font-medium px-3.5 py-1.5 rounded-lg bg-gray-100/70 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-850 transition"
 									on:click={() => {
 									on:click={() => {
 										createAPIKeyHandler();
 										createAPIKeyHandler();
 									}}
 									}}
 								>
 								>
-									<svg
-										xmlns="http://www.w3.org/2000/svg"
-										fill="none"
-										viewBox="0 0 24 24"
-										stroke-width="2"
-										stroke="currentColor"
-										class="size-4"
-									>
-										<path
-											stroke-linecap="round"
-											stroke-linejoin="round"
-											d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99"
-										/>
-									</svg>
-								</button>
-							</Tooltip>
-						{:else}
-							<button
-								class="flex gap-1.5 items-center font-medium px-3.5 py-1.5 rounded-lg bg-gray-100/70 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-850 transition"
-								on:click={() => {
-									createAPIKeyHandler();
-								}}
-							>
-								<Plus strokeWidth="2" className=" size-3.5" />
+									<Plus strokeWidth="2" className=" size-3.5" />
 
 
-								{$i18n.t('Create new secret key')}</button
-							>
-						{/if}
+									{$i18n.t('Create new secret key')}</button
+								>
+							{/if}
+						</div>
 					</div>
 					</div>
-				</div>
+				{/if}
 			</div>
 			</div>
 		{/if}
 		{/if}
 	</div>
 	</div>

+ 1 - 0
src/lib/stores/index.ts

@@ -172,6 +172,7 @@ type Config = {
 	features: {
 	features: {
 		auth: boolean;
 		auth: boolean;
 		auth_trusted_header: boolean;
 		auth_trusted_header: boolean;
+		enable_api_key: boolean;
 		enable_signup: boolean;
 		enable_signup: boolean;
 		enable_login_form: boolean;
 		enable_login_form: boolean;
 		enable_web_search?: boolean;
 		enable_web_search?: boolean;