Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions infra/main.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,30 @@ param privateEndpointVNetPrefix string = '192.168.0.0/16'
param privateEndpointSubnetAddressPrefix string = '192.168.0.0/24'
param appServiceBackendSubnetAddressPrefix string = '192.168.1.0/24'

@description('Additional OpenAI deployments to create if you want more to choose from(e.g., gpt-5, gpt-5-mini)')
param additionalLlmDeployments array = [
{
name: 'gpt-5'
model: {
name: 'gpt-5'
version: '2025-08-07'
}
sku: {
capacity: 120
}
}
{
name: 'gpt-5-mini'
model: {
name: 'gpt-5-mini'
version: '2025-08-07'
}
sku: {
capacity: 120
}
}
]

var resourceToken = toLower(uniqueString(subscription().id, name, location))
var tags = { 'azd-env-name': name }

Expand Down Expand Up @@ -134,6 +158,7 @@ module resources 'resources.bicep' = {
privateEndpointVNetPrefix: privateEndpointVNetPrefix
privateEndpointSubnetAddressPrefix: privateEndpointSubnetAddressPrefix
appServiceBackendSubnetAddressPrefix: appServiceBackendSubnetAddressPrefix
additionalLlmDeployments: additionalLlmDeployments
}
}

Expand Down
49 changes: 37 additions & 12 deletions infra/resources.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ param privateEndpointVNetPrefix string = '192.168.0.0/16'
param privateEndpointSubnetAddressPrefix string = '192.168.0.0/24'
param appServiceBackendSubnetAddressPrefix string = '192.168.1.0/24'

@description('Optional additional LLM deployments to create on the same Azure OpenAI resource')
param additionalLlmDeployments array = []

var openai_name = toLower('${name}-aillm-${resourceToken}')
var openai_dalle_name = toLower('${name}-aidalle-${resourceToken}')

Expand Down Expand Up @@ -73,7 +76,7 @@ var databaseName = 'chat'
var historyContainerName = 'history'
var configContainerName = 'config'

var llmDeployments = [
var baseLlmDeployments = [
{
name: chatGptDeploymentName
model: {
Expand All @@ -97,6 +100,23 @@ var llmDeployments = [
}
]

var mappedAdditionalDeployments = [
for addition in additionalLlmDeployments: {
name: addition.name
model: {
format: 'OpenAI'
name: addition.model.name
version: addition.model.version
}
sku: {
name: 'GlobalStandard'
capacity: addition.capacity
}
}
]

var llmDeployments = concat(baseLlmDeployments, mappedAdditionalDeployments)

module privateEndpoints 'private_endpoints_core.bicep' = if (usePrivateEndpoints) {
name: 'private-endpoints'
params: {
Expand Down Expand Up @@ -148,6 +168,10 @@ var appSettingsCommon = [
name: 'SCM_DO_BUILD_DURING_DEPLOYMENT'
value: 'true'
}
{
name: 'AZURE_OPENAI_API_EMBEDDINGS_DEPLOYMENT_NAME'
value: embeddingDeploymentName
}
{
name: 'AZURE_OPENAI_API_INSTANCE_NAME'
value: openai_name
Expand All @@ -156,10 +180,6 @@ var appSettingsCommon = [
name: 'AZURE_OPENAI_API_DEPLOYMENT_NAME'
value: chatGptDeploymentName
}
{
name: 'AZURE_OPENAI_API_EMBEDDINGS_DEPLOYMENT_NAME'
value: embeddingDeploymentName
}
{
name: 'AZURE_OPENAI_API_VERSION'
value: openai_api_version
Expand Down Expand Up @@ -214,6 +234,13 @@ var appSettingsCommon = [
}
]

var additionalModelSettingsDeployment = [
for (deployment, i) in mappedAdditionalDeployments: {
name: 'AZURE_OPENAI_API_DEPLOYMENT_NAME_MODEL_${i + 1}'
value: deployment.model.name
}
]

var appSettingsWithLocalAuth = disableLocalAuth
? []
: [
Expand Down Expand Up @@ -258,7 +285,7 @@ resource webApp 'Microsoft.Web/sites@2024-04-01' = {
appCommandLine: 'next start'
ftpsState: 'Disabled'
minTlsVersion: '1.2'
appSettings: concat(appSettingsCommon, appSettingsWithLocalAuth)
appSettings: concat(appSettingsCommon, additionalModelSettingsDeployment, appSettingsWithLocalAuth)
}
}
identity: { type: 'SystemAssigned' }
Expand Down Expand Up @@ -503,12 +530,10 @@ resource llmdeployment 'Microsoft.CognitiveServices/accounts/deployments@2023-05
model: deployment.model
/*raiPolicyName: contains(deployment, 'raiPolicyName') ? deployment.raiPolicyName : null*/
}
sku: contains(deployment, 'sku')
? deployment.sku
: {
name: 'Standard'
capacity: deployment.capacity
}
sku: deployment.?sku ?? {
name: 'Standard'
capacity: deployment.capacity
}
}
]

Expand Down
31 changes: 30 additions & 1 deletion src/features/chat-page/chat-header/chat-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ import { ChatDocumentModel, ChatThreadModel } from "../chat-services/models";
import { DocumentDetail } from "./document-detail";
import { ExtensionDetail } from "./extension-detail";
import { PersonaDetail } from "./persona-detail";
import { chatStore, useChat } from "@/features/chat-page/chat-store";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/features/ui/select";

interface Props {
chatThread: ChatThreadModel;
Expand All @@ -29,7 +37,9 @@ export const ChatHeader: FC<Props> = (props) => {
{persona}
</span>
</div>
<div className="flex gap-2">
<div className="flex gap-2 items-center">
{/* Model selector */}
<ModelSelect />
<PersonaDetail chatThread={props.chatThread} />
<DocumentDetail chatDocuments={props.chatDocuments} />
<ExtensionDetail
Expand All @@ -43,3 +53,22 @@ export const ChatHeader: FC<Props> = (props) => {
</div>
);
};

const ModelSelect: FC = () => {
const { selectedModel } = useChat();
const onChange = (v: string) => chatStore.updateSelectedModel(v);

return (
<Select value={selectedModel} onValueChange={onChange}>
<SelectTrigger className="w-44">
<SelectValue placeholder="Choose model" />
</SelectTrigger>
<SelectContent>
{/* Use your Azure OpenAI deployment names here */}
<SelectItem value="gpt-4o">gpt-4o</SelectItem>
<SelectItem value="gpt-5">gpt-5</SelectItem>
<SelectItem value="gpt-5-mini">gpt-5-mini</SelectItem>
</SelectContent>
</Select>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ export const ChatApiExtensions = async (props: {
history: ChatCompletionMessageParam[];
extensions: RunnableToolFunction<any>[];
signal: AbortSignal;
model?: string;
}): Promise<ChatCompletionStreamingRunner> => {
const { userMessage, history, signal, chatThread, extensions } = props;
const { userMessage, history, signal, chatThread, extensions, model } = props;

const openAI = OpenAIInstance();
const openAI = OpenAIInstance(model);
const systemMessage = await extensionsSystemMessage(chatThread);
return openAI.beta.chat.completions.runTools(
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ export const ChatApiMultimodal = (props: {
userMessage: string;
file: string;
signal: AbortSignal;
model?: string;
}): ChatCompletionStreamingRunner => {
const { chatThread, userMessage, signal, file } = props;
const { chatThread, userMessage, signal, file, model } = props;

const openAI = OpenAIInstance();
const openAI = OpenAIInstance(model);

return openAI.beta.chat.completions.stream(
{
Expand Down
5 changes: 3 additions & 2 deletions src/features/chat-page/chat-services/chat-api/chat-api-rag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@ export const ChatApiRAG = async (props: {
userMessage: string;
history: ChatCompletionMessageParam[];
signal: AbortSignal;
model?: string;
}): Promise<ChatCompletionStreamingRunner> => {
const { chatThread, userMessage, history, signal } = props;
const { chatThread, userMessage, history, signal, model } = props;

const openAI = OpenAIInstance();
const openAI = OpenAIInstance(model);

const documentResponse = await SimilaritySearch(
userMessage,
Expand Down
3 changes: 3 additions & 0 deletions src/features/chat-page/chat-services/chat-api/chat-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export const ChatAPIEntry = async (props: UserPrompt, signal: AbortSignal) => {
userMessage: props.message,
history: history,
signal: signal,
model: props.model,
});
break;
case "multimodal":
Expand All @@ -80,6 +81,7 @@ export const ChatAPIEntry = async (props: UserPrompt, signal: AbortSignal) => {
userMessage: props.message,
file: props.multimodalImage,
signal: signal,
model: props.model,
});
break;
case "extensions":
Expand All @@ -89,6 +91,7 @@ export const ChatAPIEntry = async (props: UserPrompt, signal: AbortSignal) => {
history: history,
extensions: extension,
signal: signal,
model: props.model,
});
break;
}
Expand Down
1 change: 1 addition & 0 deletions src/features/chat-page/chat-services/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export interface UserPrompt {
id: string; // thread id
message: string;
multimodalImage: string;
model?: string;
}

export interface ChatDocumentModel {
Expand Down
6 changes: 6 additions & 0 deletions src/features/chat-page/chat-store.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class ChatState {
public autoScroll: boolean = false;
public userName: string = "";
public chatThreadId: string = "";
public selectedModel: string = "";

private chatThread: ChatThreadModel | undefined;

Expand All @@ -58,6 +59,10 @@ class ChatState {
this.loading = value;
}

public updateSelectedModel(model: string) {
this.selectedModel = model;
}

public initChatSession({
userName,
messages,
Expand Down Expand Up @@ -284,6 +289,7 @@ class ChatState {
const body = JSON.stringify({
id: this.chatThreadId,
message: this.input,
model: this.selectedModel,
});
formData.append("content", body);

Expand Down
36 changes: 27 additions & 9 deletions src/features/common/services/openai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,46 @@ import { AzureOpenAI } from "openai";

const USE_MANAGED_IDENTITIES = process.env.USE_MANAGED_IDENTITIES === "true";

export const OpenAIInstance = () => {
export const OpenAIInstance = (deploymentOverride?: string) => {
const endpointSuffix = process.env.AZURE_OPENAI_API_ENDPOINT_SUFFIX || "openai.azure.com";
let token = process.env.AZURE_OPENAI_API_KEY;
const token = process.env.AZURE_OPENAI_API_KEY;

// Resolve model/instance/deployment based on optional override
const instanceName = process.env.AZURE_OPENAI_API_INSTANCE_NAME;
const defaultDeployment = process.env.AZURE_OPENAI_API_DEPLOYMENT_NAME;
const apiVersion = process.env.AZURE_OPENAI_API_VERSION;

let deploymentName = defaultDeployment;

if (deploymentOverride) {
for (let i = 1; i <= 100; i++) {
const key = `AZURE_OPENAI_API_DEPLOYMENT_NAME_MODEL_${i}`;
const instanceCandidate = process.env[key];
if (!instanceCandidate) continue;
if (instanceCandidate === deploymentOverride) {
deploymentName = instanceCandidate;
break;
}
}
}

if (USE_MANAGED_IDENTITIES) {
const credential = new DefaultAzureCredential();
const scope = "https://cognitiveservices.azure.com/.default";
const azureADTokenProvider = getBearerTokenProvider(credential, scope);
const deployment = process.env.AZURE_OPENAI_API_DEPLOYMENT_NAME;
const apiVersion = process.env.AZURE_OPENAI_API_VERSION;
const client = new AzureOpenAI({
azureADTokenProvider,
deployment,
deployment: deploymentName,
apiVersion,
baseURL: `https://${process.env.AZURE_OPENAI_API_INSTANCE_NAME}.${endpointSuffix}/openai/deployments/${process.env.AZURE_OPENAI_API_DEPLOYMENT_NAME}`
baseURL: `https://${instanceName}.${endpointSuffix}/openai/deployments/${deploymentName}`
});
return client;
} else {
const openai = new OpenAI({
apiKey: token,
baseURL: `https://${process.env.AZURE_OPENAI_API_INSTANCE_NAME}.${endpointSuffix}/openai/deployments/${process.env.AZURE_OPENAI_API_DEPLOYMENT_NAME}`,
defaultQuery: { "api-version": process.env.AZURE_OPENAI_API_VERSION },
defaultHeaders: { "api-key": process.env.AZURE_OPENAI_API_KEY },
baseURL: `https://${instanceName}.${endpointSuffix}/openai/deployments/${deploymentName}`,
defaultQuery: { "api-version": apiVersion },
defaultHeaders: { "api-key": token },
});
return openai;
}
Expand Down
Loading