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

Large diffs are not rendered by default.

12 changes: 11 additions & 1 deletion src/crates/core/src/infrastructure/ai/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1365,7 +1365,12 @@ impl AIClient {
let (tx, rx) = mpsc::unbounded_channel();
let (tx_raw, rx_raw) = mpsc::unbounded_channel();

tokio::spawn(handle_openai_stream(response, tx, Some(tx_raw)));
tokio::spawn(handle_openai_stream(
response,
tx,
Some(tx_raw),
self.config.inline_think_in_text,
));

return Ok(StreamResponse {
stream: Box::pin(tokio_stream::wrappers::UnboundedReceiverStream::new(rx)),
Expand Down Expand Up @@ -2092,6 +2097,7 @@ mod tests {
top_p: None,
enable_thinking_process: false,
support_preserved_thinking: false,
inline_think_in_text: false,
custom_headers: None,
custom_headers_mode: None,
skip_ssl_verify: false,
Expand All @@ -2115,6 +2121,7 @@ mod tests {
top_p: None,
enable_thinking_process: false,
support_preserved_thinking: false,
inline_think_in_text: false,
custom_headers: None,
custom_headers_mode: None,
skip_ssl_verify: false,
Expand Down Expand Up @@ -2143,6 +2150,7 @@ mod tests {
top_p: None,
enable_thinking_process: false,
support_preserved_thinking: false,
inline_think_in_text: false,
custom_headers: None,
custom_headers_mode: None,
skip_ssl_verify: false,
Expand Down Expand Up @@ -2172,6 +2180,7 @@ mod tests {
top_p: Some(0.8),
enable_thinking_process: true,
support_preserved_thinking: true,
inline_think_in_text: false,
custom_headers: None,
custom_headers_mode: None,
skip_ssl_verify: false,
Expand Down Expand Up @@ -2250,6 +2259,7 @@ mod tests {
top_p: None,
enable_thinking_process: false,
support_preserved_thinking: true,
inline_think_in_text: false,
custom_headers: None,
custom_headers_mode: None,
skip_ssl_verify: false,
Expand Down
6 changes: 6 additions & 0 deletions src/crates/core/src/service/config/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -785,6 +785,11 @@ pub struct AIModelConfig {
#[serde(default)]
pub support_preserved_thinking: bool,

/// Whether to parse OpenAI-compatible text chunks containing `<think>...</think>` into
/// streaming reasoning content.
#[serde(default)]
pub inline_think_in_text: bool,

/// Custom HTTP request headers.
#[serde(default)]
pub custom_headers: Option<std::collections::HashMap<String, String>>,
Expand Down Expand Up @@ -1196,6 +1201,7 @@ impl Default for AIModelConfig {
metadata: None,
enable_thinking_process: false,
support_preserved_thinking: false,
inline_think_in_text: false,
custom_headers: None,
custom_headers_mode: None,
skip_ssl_verify: false,
Expand Down
2 changes: 2 additions & 0 deletions src/crates/core/src/util/types/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ pub struct AIConfig {
pub top_p: Option<f64>,
pub enable_thinking_process: bool,
pub support_preserved_thinking: bool,
pub inline_think_in_text: bool,
pub custom_headers: Option<std::collections::HashMap<String, String>>,
/// "replace" (default) or "merge" (defaults first, then custom)
pub custom_headers_mode: Option<String>,
Expand Down Expand Up @@ -209,6 +210,7 @@ impl TryFrom<AIModelConfig> for AIConfig {
top_p: other.top_p,
enable_thinking_process: other.enable_thinking_process,
support_preserved_thinking: other.support_preserved_thinking,
inline_think_in_text: other.inline_think_in_text,
custom_headers: other.custom_headers,
custom_headers_mode: other.custom_headers_mode,
skip_ssl_verify: other.skip_ssl_verify,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ export const ModelConfigStep: React.FC<ModelConfigStepProps> = ({ onSkipForNow }
max_tokens: 8192,
enable_thinking_process: false,
support_preserved_thinking: false,
inline_think_in_text: false,
skip_ssl_verify: skipSslVerify,
custom_headers: Object.keys(customHeaders).length > 0 ? customHeaders : undefined,
custom_headers_mode: Object.keys(customHeaders).length > 0 ? customHeadersMode : undefined,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ class OnboardingServiceClass {
max_tokens: 8192,
category: 'general_chat',
capabilities: ['text_chat', 'function_calling'],
inline_think_in_text: false,
custom_request_body: modelConfig.customRequestBody || undefined,
skip_ssl_verify: modelConfig.skipSslVerify || undefined,
custom_headers: modelConfig.customHeaders || undefined,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const ExploreGroupRenderer: React.FC<ExploreGroupRendererProps> = ({
const {
exploreGroupStates,
onExploreGroupToggle,
onExpandGroup,
onCollapseGroup
} = useFlowChatContext();

Expand All @@ -40,6 +41,7 @@ export const ExploreGroupRenderer: React.FC<ExploreGroupRendererProps> = ({
isGroupStreaming,
isLastGroupInTurn
} = data;
const wasStreamingRef = useRef(isGroupStreaming);
const {
cardRootRef,
applyExpandedState,
Expand All @@ -53,8 +55,39 @@ export const ExploreGroupRenderer: React.FC<ExploreGroupRendererProps> = ({
),
});

const isExpanded = exploreGroupStates?.get(groupId) ?? false;
const hasExplicitState = exploreGroupStates?.has(groupId) ?? false;
const explicitExpanded = exploreGroupStates?.get(groupId) ?? false;
const isExpanded = hasExplicitState ? explicitExpanded : isGroupStreaming;
const isCollapsed = !isExpanded;
const allowManualToggle = !isGroupStreaming;

useEffect(() => {
if (isGroupStreaming && !hasExplicitState) {
applyExpandedState(false, true, () => {
onExpandGroup?.(groupId);
});
wasStreamingRef.current = true;
return;
}

if (wasStreamingRef.current && !isGroupStreaming && isExpanded) {
applyExpandedState(true, false, () => {
onCollapseGroup?.(groupId);
}, {
reason: 'auto',
});
}

wasStreamingRef.current = isGroupStreaming;
}, [
applyExpandedState,
groupId,
hasExplicitState,
isExpanded,
isGroupStreaming,
onCollapseGroup,
onExpandGroup,
]);

// Auto-scroll to bottom during streaming.
useEffect(() => {
Expand Down Expand Up @@ -105,7 +138,7 @@ export const ExploreGroupRenderer: React.FC<ExploreGroupRendererProps> = ({
// Build class list.
const className = [
'explore-region',
'explore-region--collapsible',
allowManualToggle ? 'explore-region--collapsible' : null,
isCollapsed ? 'explore-region--collapsed' : 'explore-region--expanded',
isGroupStreaming ? 'explore-region--streaming' : null,
].filter(Boolean).join(' ');
Expand All @@ -115,10 +148,12 @@ export const ExploreGroupRenderer: React.FC<ExploreGroupRendererProps> = ({
data-tool-card-id={groupId}
className={className}
>
<div className="explore-region__header" onClick={handleToggle}>
<ChevronRight size={14} className="explore-region__icon" />
<span className="explore-region__summary">{displaySummary}</span>
</div>
{allowManualToggle && (
<div className="explore-region__header" onClick={handleToggle}>
<ChevronRight size={14} className="explore-region__icon" />
<span className="explore-region__summary">{displaySummary}</span>
</div>
)}
<div className="explore-region__content-wrapper">
<div className="explore-region__content-inner">
<div ref={containerRef} className="explore-region__content">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ export interface FlowChatContextValue {
*/
onExploreGroupToggle?: (groupId: string) => void;

/**
* Expand the specified explore group.
*/
onExpandGroup?: (groupId: string) => void;

/**
* Expand all explore groups within a turn.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export const ModernFlowChatContainer: React.FC<ModernFlowChatContainerProps> = (
const {
exploreGroupStates,
onExploreGroupToggle: handleExploreGroupToggle,
onExpandGroup: handleExpandGroup,
onExpandAllInTurn: handleExpandAllInTurn,
onCollapseGroup: handleCollapseGroup,
} = useExploreGroupState(virtualItems);
Expand Down Expand Up @@ -89,6 +90,7 @@ export const ModernFlowChatContainer: React.FC<ModernFlowChatContainerProps> = (
},
exploreGroupStates,
onExploreGroupToggle: handleExploreGroupToggle,
onExpandGroup: handleExpandGroup,
onExpandAllInTurn: handleExpandAllInTurn,
onCollapseGroup: handleCollapseGroup,
}), [
Expand All @@ -102,6 +104,7 @@ export const ModernFlowChatContainer: React.FC<ModernFlowChatContainerProps> = (
config,
exploreGroupStates,
handleExploreGroupToggle,
handleExpandGroup,
handleExpandAllInTurn,
handleCollapseGroup,
]);
Expand Down
13 changes: 13 additions & 0 deletions src/web-ui/src/flow_chat/components/modern/useExploreGroupState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ interface UseExploreGroupStateResult {
*/
exploreGroupStates: Map<string, boolean>;
onExploreGroupToggle: (groupId: string) => void;
onExpandGroup: (groupId: string) => void;
onExpandAllInTurn: (turnId: string) => void;
onCollapseGroup: (groupId: string) => void;
}
Expand All @@ -32,6 +33,17 @@ export function useExploreGroupState(
});
}, []);

const onExpandGroup = useCallback((groupId: string) => {
setExploreGroupStates(prev => {
if (prev.get(groupId) === true) {
return prev;
}
const next = new Map(prev);
next.set(groupId, true);
return next;
});
}, []);

const onExpandAllInTurn = useCallback((turnId: string) => {
const groupIds = virtualItems
.filter((item): item is ExploreGroupVirtualItem => (
Expand All @@ -57,6 +69,7 @@ export function useExploreGroupState(
return {
exploreGroupStates,
onExploreGroupToggle,
onExpandGroup,
onExpandAllInTurn,
onCollapseGroup,
};
Expand Down
29 changes: 23 additions & 6 deletions src/web-ui/src/infrastructure/config/components/AIModelConfig.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,7 @@ const AIModelConfig: React.FC = () => {
metadata: config.metadata || {},
enable_thinking_process: config.enable_thinking_process ?? false,
support_preserved_thinking: config.support_preserved_thinking ?? false,
inline_think_in_text: config.inline_think_in_text ?? false,
reasoning_effort: config.reasoning_effort,
custom_headers: config.custom_headers,
custom_headers_mode: config.custom_headers_mode,
Expand All @@ -420,6 +421,7 @@ const AIModelConfig: React.FC = () => {
base_url: config.base_url,
api_key: config.api_key,
model_name: config.model_name,
inline_think_in_text: config.inline_think_in_text ?? false,
skip_ssl_verify: config.skip_ssl_verify ?? false,
custom_headers_mode: config.custom_headers_mode || null,
custom_headers: config.custom_headers || null,
Expand Down Expand Up @@ -527,7 +529,8 @@ const AIModelConfig: React.FC = () => {
category: 'general_chat',
capabilities: ['text_chat', 'function_calling'],
recommended_for: [],
metadata: {}
metadata: {},
inline_think_in_text: false,
});
setSelectedModelDrafts(
configuredProviderModels.length > 0
Expand Down Expand Up @@ -563,7 +566,8 @@ const AIModelConfig: React.FC = () => {
category: 'general_chat',
capabilities: ['text_chat'],
recommended_for: [],
metadata: {}
metadata: {},
inline_think_in_text: false,
});
setSelectedModelDrafts([]);
setShowAdvancedSettings(false);
Expand Down Expand Up @@ -597,6 +601,7 @@ const AIModelConfig: React.FC = () => {
metadata: config.metadata || {},
enable_thinking_process: config.enable_thinking_process ?? false,
support_preserved_thinking: config.support_preserved_thinking ?? false,
inline_think_in_text: config.inline_think_in_text ?? false,
reasoning_effort: config.reasoning_effort,
custom_headers: config.custom_headers,
custom_headers_mode: config.custom_headers_mode,
Expand All @@ -605,6 +610,7 @@ const AIModelConfig: React.FC = () => {
});
setSelectedModelDrafts(createDraftsFromConfigs(configuredProviderModels));
setShowAdvancedSettings(
!!config.inline_think_in_text ||
!!config.skip_ssl_verify ||
(!!config.custom_request_body && config.custom_request_body.trim() !== '') ||
(!!config.custom_headers && Object.keys(config.custom_headers).length > 0)
Expand All @@ -628,7 +634,12 @@ const AIModelConfig: React.FC = () => {

const hasCustomHeaders = !!config.custom_headers && Object.keys(config.custom_headers).length > 0;
const hasCustomBody = !!config.custom_request_body && config.custom_request_body.trim() !== '';
setShowAdvancedSettings(hasCustomHeaders || hasCustomBody || !!config.skip_ssl_verify);
setShowAdvancedSettings(
hasCustomHeaders ||
hasCustomBody ||
!!config.skip_ssl_verify ||
!!config.inline_think_in_text
);
setIsEditing(true);
};

Expand Down Expand Up @@ -680,6 +691,7 @@ const AIModelConfig: React.FC = () => {
metadata: editingConfig.metadata,
enable_thinking_process: draft.enableThinking,
support_preserved_thinking: editingConfig.support_preserved_thinking ?? false,
inline_think_in_text: editingConfig.inline_think_in_text ?? false,
reasoning_effort: editingConfig.reasoning_effort,
custom_headers: editingConfig.custom_headers,
custom_headers_mode: editingConfig.custom_headers_mode,
Expand Down Expand Up @@ -1378,6 +1390,7 @@ const AIModelConfig: React.FC = () => {
...prev,
provider,
request_url: resolveRequestUrl(prev?.base_url || '', provider, prev?.model_name || ''),
inline_think_in_text: provider === 'openai' ? (prev?.inline_think_in_text ?? false) : false,
reasoning_effort: isResponsesProvider(provider) ? (prev?.reasoning_effort || 'medium') : undefined,
}));
}} placeholder={t('form.providerPlaceholder')} options={requestFormatOptions} size="small" />
Expand Down Expand Up @@ -1458,9 +1471,13 @@ const AIModelConfig: React.FC = () => {

{showAdvancedSettings && (
<>
{editingConfig.enable_thinking_process && (
<ConfigPageRow label={t('thinking.preserve')} description={t('thinking.preserveHint')} align="center">
<Switch checked={editingConfig.support_preserved_thinking ?? false} onChange={(e) => setEditingConfig(prev => ({ ...prev, support_preserved_thinking: e.target.checked }))} size="small" />
{editingConfig.provider === 'openai' && (
<ConfigPageRow label={t('advancedSettings.inlineThinkInText.label')} description={t('advancedSettings.inlineThinkInText.hint')} align="center">
<Switch
checked={editingConfig.inline_think_in_text ?? false}
onChange={(e) => setEditingConfig(prev => ({ ...prev, inline_think_in_text: e.target.checked }))}
size="small"
/>
</ConfigPageRow>
)}
<ConfigPageRow label={t('advancedSettings.skipSslVerify.label')} align="center">
Expand Down
14 changes: 14 additions & 0 deletions src/web-ui/src/infrastructure/config/schemas/ai-models.json
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,20 @@
"title": "高级设置",
"icon": "cog",
"fields": [
{
"type": "switch",
"dataType": "boolean",
"key": "inline_think_in_text",
"title": "解析文本中的 <think> 标签",
"description": "将 OpenAI 兼容文本流中的 <think>...</think> 实时拆分为 reasoning 内容,默认关闭",
"default": false,
"conditional": {
"when": "provider",
"operator": "equals",
"value": "openai",
"show": true
}
},
{
"type": "number",
"dataType": "number",
Expand Down
3 changes: 3 additions & 0 deletions src/web-ui/src/infrastructure/config/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,9 @@ export interface AIModelConfig {

support_preserved_thinking?: boolean;

/** Parse `<think>...</think>` text chunks into streaming reasoning content. */
inline_think_in_text?: boolean;

/** Reasoning effort for OpenAI Responses API ("low" | "medium" | "high" | "xhigh") */
reasoning_effort?: string;
}
Expand Down
4 changes: 4 additions & 0 deletions src/web-ui/src/locales/en-US/settings/ai-model.json
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,10 @@
},
"advancedSettings": {
"title": "Advanced Settings",
"inlineThinkInText": {
"label": "Parse <think> Tags In Text",
"hint": "Split OpenAI-compatible text streams containing <think>...</think> into live reasoning content. Disabled by default."
},
"skipSslVerify": {
"label": "Skip SSL Certificate Verification",
"warning": "Skipping SSL verification is a security risk, use only in test or internal network environments!"
Expand Down
Loading
Loading