From e8f51fe98f788c6f6a368037a538905d7e1441a2 Mon Sep 17 00:00:00 2001 From: "Michael Ludvig (CyberCX)" Date: Thu, 9 Oct 2025 16:55:45 +1300 Subject: [PATCH 1/3] Support defaultModel config --- backend/app/routes/global_config.py | 3 +++ backend/app/usecases/global_config.py | 10 +++++++++ cdk/bin/bedrock-chat.ts | 1 + cdk/lib/bedrock-chat-stack.ts | 2 ++ cdk/lib/constructs/api.ts | 2 ++ cdk/lib/utils/parameter-models.ts | 4 ++++ frontend/src/@types/global-config.d.ts | 1 + frontend/src/hooks/useModel.ts | 30 +++++++++++++++----------- 8 files changed, 40 insertions(+), 13 deletions(-) diff --git a/backend/app/routes/global_config.py b/backend/app/routes/global_config.py index 12ea4442b..10c66bb23 100644 --- a/backend/app/routes/global_config.py +++ b/backend/app/routes/global_config.py @@ -3,6 +3,7 @@ from app.usecases.global_config import ( get_logo_path, get_global_available_models, + get_default_model, ) router = APIRouter(tags=["config"]) @@ -12,8 +13,10 @@ def get_global_config(): """Get global configuration including available models.""" global_models = get_global_available_models() + default_model = get_default_model() logo_path = get_logo_path() return { "globalAvailableModels": global_models, + "defaultModel": default_model, "logoPath": logo_path, } diff --git a/backend/app/usecases/global_config.py b/backend/app/usecases/global_config.py index 4c3fa803e..651d53afa 100644 --- a/backend/app/usecases/global_config.py +++ b/backend/app/usecases/global_config.py @@ -2,10 +2,13 @@ import logging import os +from app.routes.schemas.conversation import type_model_name + logging.basicConfig(level=logging.INFO, format="%(levelname)s:%(name)s - %(message)s") logger = logging.getLogger(__name__) GLOBAL_AVAILABLE_MODELS = os.environ.get("GLOBAL_AVAILABLE_MODELS") +DEFAULT_MODEL = os.environ.get("DEFAULT_MODEL") LOGO_PATH = os.environ.get("LOGO_PATH", "") @@ -39,6 +42,13 @@ def get_global_available_models() -> list[str]: return [] +def get_default_model() -> type_model_name: + """Return the configured default model.""" + if not DEFAULT_MODEL: + raise ValueError("DEFAULT_MODEL environment variable must be set") + return DEFAULT_MODEL # type: ignore[return-value] + + def get_logo_path() -> str: """Return the configured drawer logo path.""" return LOGO_PATH diff --git a/cdk/bin/bedrock-chat.ts b/cdk/bin/bedrock-chat.ts index 5123f7d2c..5bfd50f56 100644 --- a/cdk/bin/bedrock-chat.ts +++ b/cdk/bin/bedrock-chat.ts @@ -102,6 +102,7 @@ const chat = new BedrockChatStack( enableBotStoreReplicas: params.enableBotStoreReplicas, botStoreLanguage: params.botStoreLanguage, globalAvailableModels: params.globalAvailableModels, + defaultModel: params.defaultModel, tokenValidMinutes: params.tokenValidMinutes, devAccessIamRoleArn: params.devAccessIamRoleArn, allowedCountries: params.allowedCountries, diff --git a/cdk/lib/bedrock-chat-stack.ts b/cdk/lib/bedrock-chat-stack.ts index 8380c41f0..90aa05630 100644 --- a/cdk/lib/bedrock-chat-stack.ts +++ b/cdk/lib/bedrock-chat-stack.ts @@ -51,6 +51,7 @@ export interface BedrockChatStackProps extends StackProps { readonly enableBotStoreReplicas: boolean; readonly botStoreLanguage: Language; readonly globalAvailableModels?: string[]; + readonly defaultModel?: string; readonly tokenValidMinutes: number; readonly alternateDomainName?: string; readonly hostedZoneId?: string; @@ -225,6 +226,7 @@ export class BedrockChatStack extends cdk.Stack { enableLambdaSnapStart: props.enableLambdaSnapStart, openSearchEndpoint: botStore?.openSearchEndpoint, globalAvailableModels: props.globalAvailableModels, + defaultModel: props.defaultModel, logoPath: props.logoPath, }); props.documentBucket.grantReadWrite(backendApi.handler); diff --git a/cdk/lib/constructs/api.ts b/cdk/lib/constructs/api.ts index 70c6ad4e0..2826998bf 100644 --- a/cdk/lib/constructs/api.ts +++ b/cdk/lib/constructs/api.ts @@ -43,6 +43,7 @@ export interface ApiProps { readonly enableLambdaSnapStart: boolean; readonly openSearchEndpoint?: string; readonly globalAvailableModels?: string[]; + readonly defaultModel?: string; readonly logoPath?: string; } @@ -268,6 +269,7 @@ export class Api extends Construct { GLOBAL_AVAILABLE_MODELS: props.globalAvailableModels ? JSON.stringify(props.globalAvailableModels) : "[]", + DEFAULT_MODEL: props.defaultModel!, OPENSEARCH_DOMAIN_ENDPOINT: props.openSearchEndpoint || "", LOGO_PATH: props.logoPath || "", USE_STRANDS: "true", diff --git a/cdk/lib/utils/parameter-models.ts b/cdk/lib/utils/parameter-models.ts index a262b87cc..26ae31839 100644 --- a/cdk/lib/utils/parameter-models.ts +++ b/cdk/lib/utils/parameter-models.ts @@ -114,6 +114,9 @@ const BedrockChatParametersSchema = BaseParametersSchema.extend({ // If not configured (empty array), all models are available globalAvailableModels: z.array(z.string()).default([]), + // Default model to be selected when user first visits the app + defaultModel: z.string().default("claude-v3.7-sonnet"), + // Frontend branding logoPath: z.string().default(""), @@ -245,6 +248,7 @@ export function resolveBedrockChatParameters( enableBotStoreReplicas: app.node.tryGetContext("EnableBotStoreReplicas"), botStoreLanguage: app.node.tryGetContext("botStoreLanguage"), globalAvailableModels: app.node.tryGetContext("globalAvailableModels"), + defaultModel: app.node.tryGetContext("defaultModel"), logoPath: app.node.tryGetContext("logoPath"), devAccessIamRoleArn: app.node.tryGetContext("devAccessIamRoleArn"), }; diff --git a/frontend/src/@types/global-config.d.ts b/frontend/src/@types/global-config.d.ts index a91e9325f..355901a07 100644 --- a/frontend/src/@types/global-config.d.ts +++ b/frontend/src/@types/global-config.d.ts @@ -2,6 +2,7 @@ import { AVAILABLE_MODEL_KEYS } from '../constants/index'; export interface GlobalConfig { globalAvailableModels: string[]; + defaultModel?: string; logoPath?: string; } diff --git a/frontend/src/hooks/useModel.ts b/frontend/src/hooks/useModel.ts index 3b91fa273..f3c691bfa 100644 --- a/frontend/src/hooks/useModel.ts +++ b/frontend/src/hooks/useModel.ts @@ -30,13 +30,11 @@ const LLAMA_SUPPORTED_MEDIA_TYPES = [ 'image/webp', ]; -const DEFAULT_MODEL: Model = 'claude-v3.7-sonnet'; - const useModelState = create<{ modelId: Model; setModelId: (m: Model) => void; }>((set) => ({ - modelId: DEFAULT_MODEL, + modelId: '' as Model, // Will be set by useEffect based on config/localStorage setModelId: (m) => { set({ modelId: m, @@ -285,7 +283,7 @@ const useModel = (botId?: string | null, activeModels?: ActiveModels) => { const { modelId, setModelId } = useModelState(); const [recentUseModelId, setRecentUseModelId] = useLocalStorage( 'recentUseModelId', - DEFAULT_MODEL + '' // Will use getDefaultModel() if localStorage is empty ); // Save the model id by each bot @@ -306,16 +304,22 @@ const useModel = (botId?: string | null, activeModels?: ActiveModels) => { }, [processedActiveModels, availableModels]); const getDefaultModel = useCallback(() => { - // check default model is available - const defaultModelAvailable = filteredModels.some( - (m: ModelItem) => m.modelId === DEFAULT_MODEL - ); - if (defaultModelAvailable) { - return DEFAULT_MODEL; + // Use the default model from global config if available + const configDefaultModel = globalConfig?.defaultModel as Model | undefined; + + if (configDefaultModel) { + // Check if the configured default model is available + const defaultModelAvailable = filteredModels.some( + (m: ModelItem) => m.modelId === configDefaultModel + ); + if (defaultModelAvailable) { + return configDefaultModel; + } } - // If the default model is not available, select the first model on the list - return filteredModels[0]?.modelId ?? DEFAULT_MODEL; - }, [filteredModels]); + + // If config default is not available or not set yet, select the first model + return filteredModels[0]?.modelId; + }, [filteredModels, globalConfig?.defaultModel]); // select the model via list of activeModels const selectModel = useCallback( From 5da61ff2171eb71ec55ce5fb1cc823704ad0e193 Mon Sep 17 00:00:00 2001 From: "Michael Ludvig (CyberCX)" Date: Thu, 9 Oct 2025 17:07:00 +1300 Subject: [PATCH 2/3] Use defaultModel for titles --- backend/app/usecases/chat.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/backend/app/usecases/chat.py b/backend/app/usecases/chat.py index b8bacdae0..8e742ef55 100644 --- a/backend/app/usecases/chat.py +++ b/backend/app/usecases/chat.py @@ -46,6 +46,7 @@ ) from app.stream import ConverseApiStreamHandler, OnStopInput, OnThinking from app.usecases.bot import fetch_bot, modify_bot_last_used_time, modify_bot_stats +from app.usecases.global_config import get_default_model from app.user import User from app.utils import get_current_time from app.vector_search import ( @@ -633,8 +634,10 @@ def chat_output_from_message( def propose_conversation_title( user_id: str, conversation_id: str, - model: type_model_name = "claude-v3-haiku", ) -> str: + # Use the configured default model for generating conversation titles + model = get_default_model() + PROMPT = """Reading the conversation above, what is the appropriate title for the conversation? When answering the title, please follow the rules below: - Title length must be from 15 to 20 characters. From 96df4ae1ab361802fbb83a6dd7aad9f78b12ad25 Mon Sep 17 00:00:00 2001 From: "Michael Ludvig (CyberCX)" Date: Mon, 13 Oct 2025 13:38:26 +1300 Subject: [PATCH 3/3] Fall back to default model in drop-down --- .../src/components/SwitchBedrockModel.tsx | 30 +++++++++++++++---- frontend/src/hooks/useModel.ts | 1 + 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/SwitchBedrockModel.tsx b/frontend/src/components/SwitchBedrockModel.tsx index 1160b2645..13dc4165b 100644 --- a/frontend/src/components/SwitchBedrockModel.tsx +++ b/frontend/src/components/SwitchBedrockModel.tsx @@ -2,7 +2,7 @@ import { BaseProps } from '../@types/common'; import useModel from '../hooks/useModel'; import { Popover, Transition } from '@headlessui/react'; import { Fragment } from 'react/jsx-runtime'; -import { useMemo } from 'react'; +import { useMemo, useEffect } from 'react'; import { PiCaretDown, PiCheck } from 'react-icons/pi'; import { ActiveModels } from '../@types/bot'; import { toCamelCase } from '../utils/StringUtils'; @@ -17,6 +17,7 @@ const SwitchBedrockModel: React.FC = (props) => { availableModels: allModels, modelId, setModelId, + getDefaultModel, } = useModel(props.botId, props.activeModels); const availableModels = useMemo(() => { @@ -32,11 +33,30 @@ const SwitchBedrockModel: React.FC = (props) => { }); }, [allModels, props.activeModels]); - const modelName = useMemo(() => { - return ( - availableModels.find((model) => model.modelId === modelId)?.label ?? '' + // Automatically switch to the default model if the current model is not available + useEffect(() => { + const isCurrentModelAvailable = availableModels.some( + (model) => model.modelId === modelId ); - }, [availableModels, modelId]); + + if (!isCurrentModelAvailable && availableModels.length > 0) { + const defaultModelId = getDefaultModel(); + if (defaultModelId) { + setModelId(defaultModelId); + } + } + }, [availableModels, modelId, setModelId, getDefaultModel]); + + const modelName = useMemo(() => { + const foundModel = availableModels.find((model) => model.modelId === modelId); + if (foundModel) { + return foundModel.label; + } + // Fallback to the default model's label if the current model is not found + const defaultModelId = getDefaultModel(); + const defaultModel = availableModels.find((model) => model.modelId === defaultModelId); + return defaultModel?.label ?? ''; + }, [availableModels, modelId, getDefaultModel]); return (
diff --git a/frontend/src/hooks/useModel.ts b/frontend/src/hooks/useModel.ts index f3c691bfa..51a3c10b7 100644 --- a/frontend/src/hooks/useModel.ts +++ b/frontend/src/hooks/useModel.ts @@ -409,6 +409,7 @@ const useModel = (botId?: string | null, activeModels?: ActiveModels) => { }) ?? [], availableModels: filteredModels, forceReasoningEnabled: model?.forceReasoningEnabled ?? false, + getDefaultModel, }; };