diff --git a/application/single_app/route_backend_conversation_export.py b/application/single_app/route_backend_conversation_export.py
index 086019f1e..19b580563 100644
--- a/application/single_app/route_backend_conversation_export.py
+++ b/application/single_app/route_backend_conversation_export.py
@@ -9,7 +9,7 @@
from collections import Counter, defaultdict
from datetime import datetime
from html import escape as _escape_html
-from typing import Any, Dict, List, Optional
+from typing import Any, Dict, List, Optional, Tuple
from bs4 import BeautifulSoup, NavigableString, Tag
from config import *
@@ -56,6 +56,9 @@ def api_export_conversations():
packaging (str): Output packaging — "single" or "zip".
include_summary_intro (bool): Whether to generate a per-conversation intro.
summary_model_deployment (str): Optional model deployment for summary generation.
+ summary_model_id (str): Optional multi-endpoint model id for summary generation.
+ summary_model_endpoint_id (str): Optional multi-endpoint endpoint id for summary generation.
+ summary_model_provider (str): Optional provider (aoai/aifoundry) for summary generation.
"""
user_id = get_current_user_id()
if not user_id:
@@ -70,6 +73,9 @@ def api_export_conversations():
packaging = str(data.get('packaging', 'single')).lower()
include_summary_intro = bool(data.get('include_summary_intro', False))
summary_model_deployment = str(data.get('summary_model_deployment', '') or '').strip()
+ summary_model_id = str(data.get('summary_model_id', '') or '').strip()
+ summary_model_endpoint_id = str(data.get('summary_model_endpoint_id', '') or '').strip()
+ summary_model_provider = str(data.get('summary_model_provider', '') or '').strip()
if not conversation_ids or not isinstance(conversation_ids, list):
return jsonify({'error': 'At least one conversation_id is required'}), 400
@@ -115,7 +121,10 @@ def api_export_conversations():
user_id=user_id,
settings=settings,
include_summary_intro=include_summary_intro,
- summary_model_deployment=summary_model_deployment
+ summary_model_deployment=summary_model_deployment,
+ summary_model_id=summary_model_id,
+ summary_model_endpoint_id=summary_model_endpoint_id,
+ summary_model_provider=summary_model_provider
)
)
@@ -247,7 +256,10 @@ def _build_export_entry(
user_id: str,
settings: Dict[str, Any],
include_summary_intro: bool = False,
- summary_model_deployment: str = ''
+ summary_model_deployment: str = '',
+ summary_model_id: str = '',
+ summary_model_endpoint_id: str = '',
+ summary_model_provider: str = '',
) -> Dict[str, Any]:
artifact_payload_map = build_message_artifact_payload_map(raw_messages)
filtered_messages = _filter_messages_for_export(raw_messages)
@@ -309,6 +321,9 @@ def _build_export_entry(
settings=settings,
enabled=include_summary_intro,
summary_model_deployment=summary_model_deployment,
+ summary_model_id=summary_model_id,
+ summary_model_endpoint_id=summary_model_endpoint_id,
+ summary_model_provider=summary_model_provider,
message_time_start=message_time_start,
message_time_end=message_time_end
)
@@ -660,7 +675,12 @@ def generate_conversation_summary(
model_deployment: str,
message_time_start: str = None,
message_time_end: str = None,
- conversation_id: str = None
+ conversation_id: str = None,
+ model_id: Optional[str] = None,
+ model_endpoint_id: Optional[str] = None,
+ model_provider: Optional[str] = None,
+ user_id: Optional[str] = None,
+ active_group_ids: Optional[List[str]] = None,
) -> Dict[str, Any]:
"""Generate a conversation summary using the LLM and optionally persist it.
@@ -686,7 +706,25 @@ def generate_conversation_summary(
transcript_text = _truncate_for_summary(transcript_text)
- gpt_client, gpt_model = _initialize_gpt_client(settings, model_deployment)
+ # Prefer multi-endpoint model resolution so summaries use the same
+ # AOAI endpoint/deployment as chat for a given model selection.
+ gpt_client: AzureOpenAI
+ gpt_model: str
+
+ multi_endpoint_client = _initialize_summary_multi_endpoint_client(
+ settings,
+ model_deployment=model_deployment,
+ model_id=model_id,
+ model_endpoint_id=model_endpoint_id,
+ model_provider=model_provider,
+ user_id=user_id,
+ active_group_ids=active_group_ids or [],
+ )
+
+ if multi_endpoint_client is not None:
+ gpt_client, gpt_model = multi_endpoint_client
+ else:
+ gpt_client, gpt_model = _initialize_gpt_client(settings, model_deployment)
summary_prompt = (
"You are summarizing a conversation for an export document. "
"Read the full conversation below and write a concise summary. "
@@ -761,6 +799,9 @@ def _build_summary_intro(
settings: Dict[str, Any],
enabled: bool,
summary_model_deployment: str,
+ summary_model_id: str = '',
+ summary_model_endpoint_id: str = '',
+ summary_model_provider: str = '',
message_time_start: str = None,
message_time_end: str = None
) -> Dict[str, Any]:
@@ -809,7 +850,12 @@ def _build_summary_intro(
model_deployment=summary_model_deployment,
message_time_start=message_time_start,
message_time_end=message_time_end,
- conversation_id=conversation_id
+ conversation_id=conversation_id,
+ model_id=summary_model_id or None,
+ model_endpoint_id=summary_model_endpoint_id or None,
+ model_provider=summary_model_provider or None,
+ user_id=conversation.get('user_id'),
+ active_group_ids=[],
)
summary_intro.update({
@@ -848,6 +894,55 @@ def _truncate_for_summary(transcript_text: str) -> str:
)
+def _initialize_summary_multi_endpoint_client(
+ settings: Dict[str, Any],
+ model_deployment: str,
+ model_id: Optional[str] = None,
+ model_endpoint_id: Optional[str] = None,
+ model_provider: Optional[str] = None,
+ user_id: Optional[str] = None,
+ active_group_ids: Optional[List[str]] = None,
+) -> Optional[Tuple[AzureOpenAI, str]]:
+ """Initialize a GPT client for summaries using the multi-endpoint model config.
+
+ This mirrors the streaming model resolution path so that summaries use the
+ exact same AOAI endpoint/deployment as chat for a given model selection.
+ Returns (client, deployment) when a multi-endpoint config can be resolved,
+ otherwise None to allow fallback to the legacy single-endpoint settings.
+ """
+ try:
+ from route_backend_chats import resolve_streaming_multi_endpoint_gpt_config
+ except Exception:
+ return None
+
+ if not settings.get('enable_multi_model_endpoints', False):
+ return None
+
+ request_payload = {
+ 'model_deployment': model_deployment or '',
+ 'model_id': model_id or '',
+ 'model_endpoint_id': model_endpoint_id or '',
+ 'model_provider': model_provider or '',
+ }
+
+ try:
+ resolved = resolve_streaming_multi_endpoint_gpt_config(
+ settings,
+ request_payload,
+ user_id or '',
+ active_group_ids=active_group_ids or [],
+ allow_default_selection=False,
+ )
+ except Exception:
+ return None
+
+ if not resolved:
+ return None
+
+ gpt_client, deployment, _provider, _endpoint, _auth_settings, _api_version = resolved
+ return gpt_client, deployment
+
+
def _initialize_gpt_client(settings: Dict[str, Any], requested_model: str = ''):
enable_gpt_apim = settings.get('enable_gpt_apim', False)
diff --git a/application/single_app/route_backend_conversations.py b/application/single_app/route_backend_conversations.py
index a99e7c54b..bd64bf357 100644
--- a/application/single_app/route_backend_conversations.py
+++ b/application/single_app/route_backend_conversations.py
@@ -1000,6 +1000,9 @@ def generate_conversation_summary_api(conversation_id):
body = request.get_json(silent=True) or {}
model_deployment = body.get('model_deployment', '')
+ model_id = body.get('model_id') or None
+ model_endpoint_id = body.get('model_endpoint_id') or None
+ model_provider = body.get('model_provider') or None
# Query messages for this conversation
try:
@@ -1045,7 +1048,17 @@ def generate_conversation_summary_api(conversation_id):
model_deployment=model_deployment,
message_time_start=message_time_start,
message_time_end=message_time_end,
- conversation_id=conversation_id
+ conversation_id=conversation_id,
+ model_id=model_id,
+ model_endpoint_id=model_endpoint_id,
+ model_provider=model_provider,
+ user_id=user_id,
+ # For now, summaries use the same group context that
+ # was already captured in conversation metadata. The
+ # generate_conversation_summary helper accepts
+ # active_group_ids but does not require it, so we pass
+ # None here to avoid referencing undefined variables.
+ active_group_ids=None,
)
return jsonify({'success': True, 'summary': summary_data}), 200
diff --git a/application/single_app/static/js/chat/chat-conversation-details.js b/application/single_app/static/js/chat/chat-conversation-details.js
index 4bba53891..c3c5eac9f 100644
--- a/application/single_app/static/js/chat/chat-conversation-details.js
+++ b/application/single_app/static/js/chat/chat-conversation-details.js
@@ -673,19 +673,43 @@ function getAvailableModelOptions() {
if (!globalSelect) {
return '';
}
- let options = '';
+
+ const tempSelect = document.createElement('select');
+
for (const opt of globalSelect.options) {
- options += ``;
+ const cloned = document.createElement('option');
+ cloned.value = opt.value;
+ cloned.textContent = opt.text;
+
+ if (opt.dataset?.deploymentName) {
+ cloned.dataset.deploymentName = opt.dataset.deploymentName;
+ }
+ if (opt.dataset?.modelId) {
+ cloned.dataset.modelId = opt.dataset.modelId;
+ }
+ if (opt.dataset?.endpointId) {
+ cloned.dataset.endpointId = opt.dataset.endpointId;
+ }
+ if (opt.dataset?.provider) {
+ cloned.dataset.provider = opt.dataset.provider;
+ }
+ if (opt.selected) {
+ cloned.selected = true;
+ }
+
+ tempSelect.appendChild(cloned);
}
- return options || '';
+
+ const html = tempSelect.innerHTML;
+ return html || '';
}
/**
* Handle summary generation (generate or regenerate)
* @param {string} conversationId - The conversation ID
- * @param {string} modelDeployment - Selected model deployment
+ * @param {Object} modelPayload - Selected model metadata
*/
-async function handleGenerateSummary(conversationId, modelDeployment) {
+async function handleGenerateSummary(conversationId, modelPayload) {
const cardBody = document.getElementById('summary-card-body');
if (!cardBody) {
return;
@@ -704,7 +728,7 @@ async function handleGenerateSummary(conversationId, modelDeployment) {
const response = await fetch(`/api/conversations/${conversationId}/summary`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ model_deployment: modelDeployment })
+ body: JSON.stringify(modelPayload || {})
});
if (!response.ok) {
@@ -766,8 +790,18 @@ document.addEventListener('click', function(e) {
const cid = btn.getAttribute('data-conversation-id');
const modelSelect = document.getElementById('summary-model-select');
const selectedOption = modelSelect ? modelSelect.options[modelSelect.selectedIndex] : null;
- const model = selectedOption?.dataset?.deploymentName || (modelSelect ? modelSelect.value : '');
- handleGenerateSummary(cid, model);
+
+ const modelDeployment = selectedOption?.dataset?.deploymentName || (modelSelect ? modelSelect.value : '');
+ const modelId = selectedOption?.dataset?.modelId || '';
+ const modelEndpointId = selectedOption?.dataset?.endpointId || '';
+ const modelProvider = selectedOption?.dataset?.provider || '';
+
+ handleGenerateSummary(cid, {
+ model_deployment: modelDeployment,
+ model_id: modelId,
+ model_endpoint_id: modelEndpointId,
+ model_provider: modelProvider
+ });
return;
}
@@ -779,8 +813,17 @@ document.addEventListener('click', function(e) {
// Use the currently selected global model for regeneration
const globalSelect = document.getElementById('model-select');
const selectedOption = globalSelect ? globalSelect.options[globalSelect.selectedIndex] : null;
- const model = selectedOption?.dataset?.deploymentName || (globalSelect ? globalSelect.value : '');
- handleGenerateSummary(cid, model);
+ const modelDeployment = selectedOption?.dataset?.deploymentName || (globalSelect ? globalSelect.value : '');
+ const modelId = selectedOption?.dataset?.modelId || '';
+ const modelEndpointId = selectedOption?.dataset?.endpointId || '';
+ const modelProvider = selectedOption?.dataset?.provider || '';
+
+ handleGenerateSummary(cid, {
+ model_deployment: modelDeployment,
+ model_id: modelId,
+ model_endpoint_id: modelEndpointId,
+ model_provider: modelProvider
+ });
return;
}
diff --git a/application/single_app/static/js/chat/chat-export.js b/application/single_app/static/js/chat/chat-export.js
index f42e0123f..66989d861 100644
--- a/application/single_app/static/js/chat/chat-export.js
+++ b/application/single_app/static/js/chat/chat-export.js
@@ -17,6 +17,9 @@ let exportFormat = 'json';
let exportPackaging = 'single';
let includeSummaryIntro = false;
let summaryModelDeployment = '';
+let summaryModelId = '';
+let summaryModelEndpointId = '';
+let summaryModelProvider = '';
let currentStep = 1;
let totalSteps = 3;
let skipSelectionStep = false;
@@ -57,6 +60,10 @@ function openExportWizard(conversationIds, skipSelection) {
exportPackaging = conversationIds.length > 1 ? 'zip' : 'single';
includeSummaryIntro = false;
summaryModelDeployment = _getDefaultSummaryModel();
+ const defaultModelMeta = _getDefaultSummaryModelMetadata();
+ summaryModelId = defaultModelMeta.modelId;
+ summaryModelEndpointId = defaultModelMeta.endpointId;
+ summaryModelProvider = defaultModelMeta.provider;
skipSelectionStep = !!skipSelection;
// Determine step configuration
@@ -345,9 +352,17 @@ function _renderSummaryStep(container) {
if (summaryModelSelect && hasModelOptions) {
summaryModelSelect.value = defaultSummaryModel || summaryModelSelect.value;
- summaryModelDeployment = summaryModelSelect.options[summaryModelSelect.selectedIndex]?.dataset?.deploymentName || summaryModelSelect.value;
+ const selected = summaryModelSelect.options[summaryModelSelect.selectedIndex];
+ summaryModelDeployment = selected?.dataset?.deploymentName || summaryModelSelect.value;
+ summaryModelId = selected?.dataset?.modelId || '';
+ summaryModelEndpointId = selected?.dataset?.endpointId || '';
+ summaryModelProvider = selected?.dataset?.provider || '';
summaryModelSelect.addEventListener('change', () => {
- summaryModelDeployment = summaryModelSelect.options[summaryModelSelect.selectedIndex]?.dataset?.deploymentName || summaryModelSelect.value;
+ const opt = summaryModelSelect.options[summaryModelSelect.selectedIndex];
+ summaryModelDeployment = opt?.dataset?.deploymentName || summaryModelSelect.value;
+ summaryModelId = opt?.dataset?.modelId || '';
+ summaryModelEndpointId = opt?.dataset?.endpointId || '';
+ summaryModelProvider = opt?.dataset?.provider || '';
});
}
@@ -359,7 +374,11 @@ function _renderSummaryStep(container) {
}
if (includeSummaryIntro && summaryModelSelect && !summaryModelSelect.value) {
summaryModelSelect.value = _getDefaultSummaryModel();
- summaryModelDeployment = summaryModelSelect.options[summaryModelSelect.selectedIndex]?.dataset?.deploymentName || summaryModelSelect.value;
+ const opt = summaryModelSelect.options[summaryModelSelect.selectedIndex];
+ summaryModelDeployment = opt?.dataset?.deploymentName || summaryModelSelect.value;
+ summaryModelId = opt?.dataset?.modelId || '';
+ summaryModelEndpointId = opt?.dataset?.endpointId || '';
+ summaryModelProvider = opt?.dataset?.provider || '';
}
});
}
@@ -532,7 +551,10 @@ async function _executeExport() {
format: exportFormat,
packaging: exportPackaging,
include_summary_intro: includeSummaryIntro,
- summary_model_deployment: includeSummaryIntro ? summaryModelDeployment : null
+ summary_model_deployment: includeSummaryIntro ? summaryModelDeployment : null,
+ summary_model_id: includeSummaryIntro ? summaryModelId : null,
+ summary_model_endpoint_id: includeSummaryIntro ? summaryModelEndpointId : null,
+ summary_model_provider: includeSummaryIntro ? summaryModelProvider : null
})
});
@@ -605,6 +627,20 @@ function _getDefaultSummaryModel() {
return mainModelSelect.value || (mainModelSelect.options[0] ? mainModelSelect.options[0].value : '');
}
+function _getDefaultSummaryModelMetadata() {
+ const mainModelSelect = getEl('model-select');
+ if (!mainModelSelect || !mainModelSelect.options.length) {
+ return { modelId: '', endpointId: '', provider: '' };
+ }
+
+ const selected = mainModelSelect.options[mainModelSelect.selectedIndex] || mainModelSelect.options[0];
+ return {
+ modelId: selected.dataset?.modelId || '',
+ endpointId: selected.dataset?.endpointId || '',
+ provider: selected.dataset?.provider || ''
+ };
+}
+
// --- Expose Globally ---
window.chatExport = {
openExportWizard
diff --git a/docs/explanation/release_notes.md b/docs/explanation/release_notes.md
index db17633db..5a6834fc3 100644
--- a/docs/explanation/release_notes.md
+++ b/docs/explanation/release_notes.md
@@ -11,6 +11,12 @@ For feature-focused and fix-focused drill-downs by version, see [Features by Ver
## Bug Fixes
+* **Conversation Summary and Export Multi-Endpoint Model Alignment**
+ * Fixed conversation summary generation so the on-demand summary card and conversation export intros now resolve GPT models through the same multi-endpoint configuration used by streaming chat.
+ * Summary and export flows now send the selected model's deployment, model id, endpoint id, and provider from the chat UI, ensuring 5.1 and other multi-endpoint models use the correct Azure OpenAI or Foundry endpoint instead of falling back to a legacy single-endpoint configuration that could return `DeploymentNotFound`.
+ * Added shared helpers so both summary and export paths prefer the multi-endpoint resolver and only fall back to the legacy single-endpoint settings when multi-endpoint is disabled, keeping model behavior consistent across chat, summaries, and exports.
+ * (Ref: `chat-conversation-details.js`, `chat-export.js`, `route_backend_conversations.py`, `route_backend_conversation_export.py`, multi-endpoint model resolution for summaries and exports)
+
* **Uploaded File Preview Body XSS Hardening**
* Fixed the uploaded-file preview modal so stored file bodies no longer reach the preview pane through raw HTML sinks.
* Plain-text previews now render as inert preformatted text, CSV-backed previews are built with DOM text nodes, and legacy HTML-backed table payloads now fall back to inert text instead of live markup.