diff --git a/backend/app/gateway/routers/models.py b/backend/app/gateway/routers/models.py
index 269b55c..6579230 100644
--- a/backend/app/gateway/routers/models.py
+++ b/backend/app/gateway/routers/models.py
@@ -10,6 +10,7 @@ class ModelResponse(BaseModel):
"""Response model for model information."""
name: str = Field(..., description="Unique identifier for the model")
+ model: str = Field(..., description="Actual provider model identifier")
display_name: str | None = Field(None, description="Human-readable name")
description: str | None = Field(None, description="Model description")
supports_thinking: bool = Field(default=False, description="Whether model supports thinking mode")
@@ -61,6 +62,7 @@ async def list_models() -> ModelsListResponse:
models = [
ModelResponse(
name=model.name,
+ model=model.model,
display_name=model.display_name,
description=model.description,
supports_thinking=model.supports_thinking,
@@ -106,6 +108,7 @@ async def get_model(model_name: str) -> ModelResponse:
return ModelResponse(
name=model.name,
+ model=model.model,
display_name=model.display_name,
description=model.description,
supports_thinking=model.supports_thinking,
diff --git a/backend/packages/harness/deerflow/client.py b/backend/packages/harness/deerflow/client.py
index 4dbd28e..983aa9f 100644
--- a/backend/packages/harness/deerflow/client.py
+++ b/backend/packages/harness/deerflow/client.py
@@ -403,6 +403,7 @@ class DeerFlowClient:
"models": [
{
"name": model.name,
+ "model": getattr(model, "model", None),
"display_name": getattr(model, "display_name", None),
"description": getattr(model, "description", None),
"supports_thinking": getattr(model, "supports_thinking", False),
@@ -462,6 +463,7 @@ class DeerFlowClient:
return None
return {
"name": model.name,
+ "model": getattr(model, "model", None),
"display_name": getattr(model, "display_name", None),
"description": getattr(model, "description", None),
"supports_thinking": getattr(model, "supports_thinking", False),
diff --git a/backend/packages/harness/deerflow/models/patched_minimax.py b/backend/packages/harness/deerflow/models/patched_minimax.py
new file mode 100644
index 0000000..69fbb00
--- /dev/null
+++ b/backend/packages/harness/deerflow/models/patched_minimax.py
@@ -0,0 +1,226 @@
+"""Patched ChatOpenAI adapter for MiniMax reasoning output.
+
+MiniMax's OpenAI-compatible chat completions API can return structured
+``reasoning_details`` when ``extra_body.reasoning_split=true`` is enabled.
+``langchain_openai.ChatOpenAI`` currently ignores that field, so DeerFlow's
+frontend never receives reasoning content in the shape it expects.
+
+This adapter preserves ``reasoning_split`` in the request payload and maps the
+provider-specific reasoning field into ``additional_kwargs.reasoning_content``,
+which DeerFlow already understands.
+"""
+
+from __future__ import annotations
+
+import re
+from collections.abc import Mapping
+from typing import Any
+
+from langchain_core.language_models import LanguageModelInput
+from langchain_core.messages import AIMessage, AIMessageChunk
+from langchain_core.outputs import ChatGeneration, ChatGenerationChunk, ChatResult
+from langchain_openai import ChatOpenAI
+from langchain_openai.chat_models.base import (
+ _convert_delta_to_message_chunk,
+ _create_usage_metadata,
+)
+
+_THINK_TAG_RE = re.compile(r"\s*(.*?)\s*", re.DOTALL)
+
+
+def _extract_reasoning_text(
+ reasoning_details: Any,
+ *,
+ strip_parts: bool = True,
+) -> str | None:
+ if not isinstance(reasoning_details, list):
+ return None
+
+ parts: list[str] = []
+ for item in reasoning_details:
+ if not isinstance(item, Mapping):
+ continue
+ text = item.get("text")
+ if isinstance(text, str):
+ normalized = text.strip() if strip_parts else text
+ if normalized.strip():
+ parts.append(normalized)
+
+ return "\n\n".join(parts) if parts else None
+
+
+def _strip_inline_think_tags(content: str) -> tuple[str, str | None]:
+ reasoning_parts: list[str] = []
+
+ def _replace(match: re.Match[str]) -> str:
+ reasoning = match.group(1).strip()
+ if reasoning:
+ reasoning_parts.append(reasoning)
+ return ""
+
+ cleaned = _THINK_TAG_RE.sub(_replace, content).strip()
+ reasoning = "\n\n".join(reasoning_parts) if reasoning_parts else None
+ return cleaned, reasoning
+
+
+def _merge_reasoning(*values: str | None) -> str | None:
+ merged: list[str] = []
+ for value in values:
+ if not value:
+ continue
+ normalized = value.strip()
+ if normalized and normalized not in merged:
+ merged.append(normalized)
+ return "\n\n".join(merged) if merged else None
+
+
+def _with_reasoning_content(
+ message: AIMessage | AIMessageChunk,
+ reasoning: str | None,
+ *,
+ preserve_whitespace: bool = False,
+):
+ if not reasoning:
+ return message
+
+ additional_kwargs = dict(message.additional_kwargs)
+ if preserve_whitespace:
+ existing = additional_kwargs.get("reasoning_content")
+ additional_kwargs["reasoning_content"] = (
+ f"{existing}{reasoning}" if isinstance(existing, str) else reasoning
+ )
+ else:
+ additional_kwargs["reasoning_content"] = _merge_reasoning(
+ additional_kwargs.get("reasoning_content"),
+ reasoning,
+ )
+ return message.model_copy(update={"additional_kwargs": additional_kwargs})
+
+
+class PatchedChatMiniMax(ChatOpenAI):
+ """ChatOpenAI adapter that preserves MiniMax reasoning output."""
+
+ def _get_request_payload(
+ self,
+ input_: LanguageModelInput,
+ *,
+ stop: list[str] | None = None,
+ **kwargs: Any,
+ ) -> dict:
+ payload = super()._get_request_payload(input_, stop=stop, **kwargs)
+ extra_body = payload.get("extra_body")
+ if isinstance(extra_body, dict):
+ payload["extra_body"] = {
+ **extra_body,
+ "reasoning_split": True,
+ }
+ else:
+ payload["extra_body"] = {"reasoning_split": True}
+ return payload
+
+ def _convert_chunk_to_generation_chunk(
+ self,
+ chunk: dict,
+ default_chunk_class: type,
+ base_generation_info: dict | None,
+ ) -> ChatGenerationChunk | None:
+ if chunk.get("type") == "content.delta":
+ return None
+
+ token_usage = chunk.get("usage")
+ choices = chunk.get("choices", []) or chunk.get("chunk", {}).get("choices", [])
+ usage_metadata = (
+ _create_usage_metadata(token_usage, chunk.get("service_tier"))
+ if token_usage
+ else None
+ )
+
+ if len(choices) == 0:
+ generation_chunk = ChatGenerationChunk(
+ message=default_chunk_class(content="", usage_metadata=usage_metadata),
+ generation_info=base_generation_info,
+ )
+ if self.output_version == "v1":
+ generation_chunk.message.content = []
+ generation_chunk.message.response_metadata["output_version"] = "v1"
+ return generation_chunk
+
+ choice = choices[0]
+ delta = choice.get("delta")
+ if delta is None:
+ return None
+
+ message_chunk = _convert_delta_to_message_chunk(delta, default_chunk_class)
+ generation_info = {**base_generation_info} if base_generation_info else {}
+
+ if finish_reason := choice.get("finish_reason"):
+ generation_info["finish_reason"] = finish_reason
+ if model_name := chunk.get("model"):
+ generation_info["model_name"] = model_name
+ if system_fingerprint := chunk.get("system_fingerprint"):
+ generation_info["system_fingerprint"] = system_fingerprint
+ if service_tier := chunk.get("service_tier"):
+ generation_info["service_tier"] = service_tier
+
+ logprobs = choice.get("logprobs")
+ if logprobs:
+ generation_info["logprobs"] = logprobs
+
+ reasoning = _extract_reasoning_text(
+ delta.get("reasoning_details"),
+ strip_parts=False,
+ )
+ if isinstance(message_chunk, AIMessageChunk):
+ if usage_metadata:
+ message_chunk.usage_metadata = usage_metadata
+ if reasoning:
+ message_chunk = _with_reasoning_content(
+ message_chunk,
+ reasoning,
+ preserve_whitespace=True,
+ )
+
+ message_chunk.response_metadata["model_provider"] = "openai"
+ return ChatGenerationChunk(
+ message=message_chunk,
+ generation_info=generation_info or None,
+ )
+
+ def _create_chat_result(
+ self,
+ response: dict | Any,
+ generation_info: dict | None = None,
+ ) -> ChatResult:
+ result = super()._create_chat_result(response, generation_info)
+ response_dict = response if isinstance(response, dict) else response.model_dump()
+ choices = response_dict.get("choices", [])
+
+ generations: list[ChatGeneration] = []
+ for index, generation in enumerate(result.generations):
+ choice = choices[index] if index < len(choices) else {}
+ message = generation.message
+ if isinstance(message, AIMessage):
+ content = message.content if isinstance(message.content, str) else None
+ cleaned_content = content
+ inline_reasoning = None
+ if isinstance(content, str):
+ cleaned_content, inline_reasoning = _strip_inline_think_tags(content)
+
+ choice_message = choice.get("message", {}) if isinstance(choice, Mapping) else {}
+ split_reasoning = _extract_reasoning_text(choice_message.get("reasoning_details"))
+ merged_reasoning = _merge_reasoning(split_reasoning, inline_reasoning)
+
+ updated_message = message
+ if cleaned_content is not None and cleaned_content != message.content:
+ updated_message = updated_message.model_copy(update={"content": cleaned_content})
+ if merged_reasoning:
+ updated_message = _with_reasoning_content(updated_message, merged_reasoning)
+
+ generation = ChatGeneration(
+ message=updated_message,
+ generation_info=generation.generation_info,
+ )
+
+ generations.append(generation)
+
+ return ChatResult(generations=generations, llm_output=result.llm_output)
diff --git a/backend/tests/test_client.py b/backend/tests/test_client.py
index 62742d5..f0b5d21 100644
--- a/backend/tests/test_client.py
+++ b/backend/tests/test_client.py
@@ -28,6 +28,7 @@ def mock_app_config():
"""Provide a minimal AppConfig mock."""
model = MagicMock()
model.name = "test-model"
+ model.model = "test-model"
model.supports_thinking = False
model.supports_reasoning_effort = False
model.model_dump.return_value = {"name": "test-model", "use": "langchain_openai:ChatOpenAI"}
@@ -98,6 +99,7 @@ class TestConfigQueries:
assert len(result["models"]) == 1
assert result["models"][0]["name"] == "test-model"
# Verify Gateway-aligned fields are present
+ assert "model" in result["models"][0]
assert "display_name" in result["models"][0]
assert "supports_thinking" in result["models"][0]
@@ -420,6 +422,7 @@ class TestGetModel:
def test_found(self, client):
model_cfg = MagicMock()
model_cfg.name = "test-model"
+ model_cfg.model = "test-model"
model_cfg.display_name = "Test Model"
model_cfg.description = "A test model"
model_cfg.supports_thinking = True
@@ -429,6 +432,7 @@ class TestGetModel:
result = client.get_model("test-model")
assert result == {
"name": "test-model",
+ "model": "test-model",
"display_name": "Test Model",
"description": "A test model",
"supports_thinking": True,
@@ -1048,6 +1052,7 @@ class TestScenarioConfigManagement:
# Get specific model
model_cfg = MagicMock()
model_cfg.name = model_name
+ model_cfg.model = model_name
model_cfg.display_name = None
model_cfg.description = None
model_cfg.supports_thinking = False
@@ -1503,6 +1508,7 @@ class TestGatewayConformance:
def test_list_models(self, mock_app_config):
model = MagicMock()
model.name = "test-model"
+ model.model = "gpt-test"
model.display_name = "Test Model"
model.description = "A test model"
model.supports_thinking = False
@@ -1515,10 +1521,12 @@ class TestGatewayConformance:
parsed = ModelsListResponse(**result)
assert len(parsed.models) == 1
assert parsed.models[0].name == "test-model"
+ assert parsed.models[0].model == "gpt-test"
def test_get_model(self, mock_app_config):
model = MagicMock()
model.name = "test-model"
+ model.model = "gpt-test"
model.display_name = "Test Model"
model.description = "A test model"
model.supports_thinking = True
@@ -1532,6 +1540,7 @@ class TestGatewayConformance:
assert result is not None
parsed = ModelResponse(**result)
assert parsed.name == "test-model"
+ assert parsed.model == "gpt-test"
def test_list_skills(self, client):
skill = MagicMock()
diff --git a/backend/tests/test_patched_minimax.py b/backend/tests/test_patched_minimax.py
new file mode 100644
index 0000000..c95065b
--- /dev/null
+++ b/backend/tests/test_patched_minimax.py
@@ -0,0 +1,149 @@
+from langchain_core.messages import AIMessageChunk, HumanMessage
+
+from deerflow.models.patched_minimax import PatchedChatMiniMax
+
+
+def _make_model(**kwargs) -> PatchedChatMiniMax:
+ return PatchedChatMiniMax(
+ model="MiniMax-M2.5",
+ api_key="test-key",
+ base_url="https://example.com/v1",
+ **kwargs,
+ )
+
+
+def test_get_request_payload_preserves_thinking_and_forces_reasoning_split():
+ model = _make_model(extra_body={"thinking": {"type": "disabled"}})
+
+ payload = model._get_request_payload([HumanMessage(content="hello")])
+
+ assert payload["extra_body"]["thinking"]["type"] == "disabled"
+ assert payload["extra_body"]["reasoning_split"] is True
+
+
+def test_create_chat_result_maps_reasoning_details_to_reasoning_content():
+ model = _make_model()
+ response = {
+ "choices": [
+ {
+ "message": {
+ "role": "assistant",
+ "content": "最终答案",
+ "reasoning_details": [
+ {
+ "type": "reasoning.text",
+ "id": "reasoning-text-1",
+ "format": "MiniMax-response-v1",
+ "index": 0,
+ "text": "先分析问题,再给出答案。",
+ }
+ ],
+ },
+ "finish_reason": "stop",
+ }
+ ],
+ "model": "MiniMax-M2.5",
+ }
+
+ result = model._create_chat_result(response)
+ message = result.generations[0].message
+
+ assert message.content == "最终答案"
+ assert message.additional_kwargs["reasoning_content"] == "先分析问题,再给出答案。"
+ assert result.generations[0].text == "最终答案"
+
+
+def test_create_chat_result_strips_inline_think_tags():
+ model = _make_model()
+ response = {
+ "choices": [
+ {
+ "message": {
+ "role": "assistant",
+ "content": "\n这是思考过程。\n\n\n真正回答。",
+ },
+ "finish_reason": "stop",
+ }
+ ],
+ "model": "MiniMax-M2.5",
+ }
+
+ result = model._create_chat_result(response)
+ message = result.generations[0].message
+
+ assert message.content == "真正回答。"
+ assert message.additional_kwargs["reasoning_content"] == "这是思考过程。"
+ assert result.generations[0].text == "真正回答。"
+
+
+def test_convert_chunk_to_generation_chunk_preserves_reasoning_deltas():
+ model = _make_model()
+ first = model._convert_chunk_to_generation_chunk(
+ {
+ "choices": [
+ {
+ "delta": {
+ "role": "assistant",
+ "content": "",
+ "reasoning_details": [
+ {
+ "type": "reasoning.text",
+ "id": "reasoning-text-1",
+ "format": "MiniMax-response-v1",
+ "index": 0,
+ "text": "The user",
+ }
+ ],
+ }
+ }
+ ]
+ },
+ AIMessageChunk,
+ {},
+ )
+ second = model._convert_chunk_to_generation_chunk(
+ {
+ "choices": [
+ {
+ "delta": {
+ "content": "",
+ "reasoning_details": [
+ {
+ "type": "reasoning.text",
+ "id": "reasoning-text-1",
+ "format": "MiniMax-response-v1",
+ "index": 0,
+ "text": " asks.",
+ }
+ ],
+ }
+ }
+ ]
+ },
+ AIMessageChunk,
+ {},
+ )
+ answer = model._convert_chunk_to_generation_chunk(
+ {
+ "choices": [
+ {
+ "delta": {
+ "content": "最终答案",
+ },
+ "finish_reason": "stop",
+ }
+ ],
+ "model": "MiniMax-M2.5",
+ },
+ AIMessageChunk,
+ {},
+ )
+
+ assert first is not None
+ assert second is not None
+ assert answer is not None
+
+ combined = first.message + second.message + answer.message
+
+ assert combined.additional_kwargs["reasoning_content"] == "The user asks."
+ assert combined.content == "最终答案"
diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx
index 60f2581..c36c68f 100644
--- a/frontend/src/app/layout.tsx
+++ b/frontend/src/app/layout.tsx
@@ -2,7 +2,6 @@ import "@/styles/globals.css";
import "katex/dist/katex.min.css";
import { type Metadata } from "next";
-import { Geist } from "next/font/google";
import { ThemeProvider } from "@/components/theme-provider";
import { I18nProvider } from "@/core/i18n/context";
@@ -13,22 +12,12 @@ export const metadata: Metadata = {
description: "A LangChain-based framework for building super agents.",
};
-const geist = Geist({
- subsets: ["latin"],
- variable: "--font-geist-sans",
-});
-
export default async function RootLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
const locale = await detectLocaleServer();
return (
-
+
{children}
diff --git a/frontend/src/app/mock/api/models/route.ts b/frontend/src/app/mock/api/models/route.ts
index 5768e16..e0a2eb1 100644
--- a/frontend/src/app/mock/api/models/route.ts
+++ b/frontend/src/app/mock/api/models/route.ts
@@ -4,24 +4,28 @@ export function GET() {
{
id: "doubao-seed-1.8",
name: "doubao-seed-1.8",
+ model: "doubao-seed-1-8",
display_name: "Doubao Seed 1.8",
supports_thinking: true,
},
{
id: "deepseek-v3.2",
name: "deepseek-v3.2",
+ model: "deepseek-chat",
display_name: "DeepSeek v3.2",
supports_thinking: true,
},
{
id: "gpt-5",
name: "gpt-5",
+ model: "gpt-5",
display_name: "GPT-5",
supports_thinking: true,
},
{
id: "gemini-3-pro",
name: "gemini-3-pro",
+ model: "gemini-3-pro",
display_name: "Gemini 3 Pro",
supports_thinking: true,
},
diff --git a/frontend/src/app/workspace/agents/[agent_name]/chats/[thread_id]/page.tsx b/frontend/src/app/workspace/agents/[agent_name]/chats/[thread_id]/page.tsx
index a44ad3f..aaf09f2 100644
--- a/frontend/src/app/workspace/agents/[agent_name]/chats/[thread_id]/page.tsx
+++ b/frontend/src/app/workspace/agents/[agent_name]/chats/[thread_id]/page.tsx
@@ -154,7 +154,13 @@ export default function AgentChatPage() {
isNewThread={isNewThread}
threadId={threadId}
autoFocus={isNewThread}
- status={thread.isLoading ? "streaming" : "ready"}
+ status={
+ thread.error
+ ? "error"
+ : thread.isLoading
+ ? "streaming"
+ : "ready"
+ }
context={settings.context}
extraHeader={
isNewThread && (
diff --git a/frontend/src/app/workspace/chats/[thread_id]/page.tsx b/frontend/src/app/workspace/chats/[thread_id]/page.tsx
index d47853c..26af3f5 100644
--- a/frontend/src/app/workspace/chats/[thread_id]/page.tsx
+++ b/frontend/src/app/workspace/chats/[thread_id]/page.tsx
@@ -122,7 +122,13 @@ export default function ChatPage() {
isNewThread={isNewThread}
threadId={threadId}
autoFocus={isNewThread}
- status={thread.isLoading ? "streaming" : "ready"}
+ status={
+ thread.error
+ ? "error"
+ : thread.isLoading
+ ? "streaming"
+ : "ready"
+ }
context={settings.context}
extraHeader={
isNewThread &&
diff --git a/frontend/src/components/workspace/input-box.tsx b/frontend/src/components/workspace/input-box.tsx
index b6e6a68..ef5c7dc 100644
--- a/frontend/src/components/workspace/input-box.tsx
+++ b/frontend/src/components/workspace/input-box.tsx
@@ -702,9 +702,16 @@ export function InputBox({
>
-
- {selectedModel?.display_name}
-
+
+
+ {selectedModel?.display_name}
+
+ {selectedModel?.model && (
+
+ {selectedModel.model}
+
+ )}
+
@@ -716,7 +723,12 @@ export function InputBox({
value={m.name}
onSelect={() => handleModelSelect(m.name)}
>
- {m.display_name}
+
+ {m.display_name}
+
+ {m.model}
+
+
{m.name === context.model_name ? (
) : (
diff --git a/frontend/src/core/messages/utils.ts b/frontend/src/core/messages/utils.ts
index 3e341cc..57e05bb 100644
--- a/frontend/src/core/messages/utils.ts
+++ b/frontend/src/core/messages/utils.ts
@@ -127,7 +127,7 @@ export function groupMessages(
export function extractTextFromMessage(message: Message) {
if (typeof message.content === "string") {
- return message.content.trim();
+ return splitInlineReasoningFromAIMessage(message)?.content ?? message.content.trim();
}
if (Array.isArray(message.content)) {
return message.content
@@ -138,9 +138,36 @@ export function extractTextFromMessage(message: Message) {
return "";
}
+const THINK_TAG_RE = /\s*([\s\S]*?)\s*<\/think>/g;
+
+function splitInlineReasoning(content: string) {
+ const reasoningParts: string[] = [];
+ const cleaned = content
+ .replace(THINK_TAG_RE, (_, reasoning: string) => {
+ const normalized = reasoning.trim();
+ if (normalized) {
+ reasoningParts.push(normalized);
+ }
+ return "";
+ })
+ .trim();
+
+ return {
+ content: cleaned,
+ reasoning: reasoningParts.length > 0 ? reasoningParts.join("\n\n") : null,
+ };
+}
+
+function splitInlineReasoningFromAIMessage(message: Message) {
+ if (message.type !== "ai" || typeof message.content !== "string") {
+ return null;
+ }
+ return splitInlineReasoning(message.content);
+}
+
export function extractContentFromMessage(message: Message) {
if (typeof message.content === "string") {
- return message.content.trim();
+ return splitInlineReasoningFromAIMessage(message)?.content ?? message.content.trim();
}
if (Array.isArray(message.content)) {
return message.content
@@ -177,6 +204,9 @@ export function extractReasoningContentFromMessage(message: Message) {
return part.thinking as string;
}
}
+ if (typeof message.content === "string") {
+ return splitInlineReasoning(message.content).reasoning;
+ }
return null;
}
@@ -202,7 +232,9 @@ export function extractURLFromImageURLContent(
export function hasContent(message: Message) {
if (typeof message.content === "string") {
- return message.content.trim().length > 0;
+ return (
+ splitInlineReasoningFromAIMessage(message)?.content ?? message.content.trim()
+ ).length > 0;
}
if (Array.isArray(message.content)) {
return message.content.length > 0;
@@ -222,6 +254,9 @@ export function hasReasoning(message: Message) {
// Compatible with the Anthropic gateway
return (part as unknown as { type: "thinking" })?.type === "thinking";
}
+ if (typeof message.content === "string") {
+ return splitInlineReasoning(message.content).reasoning !== null;
+ }
return false;
}
diff --git a/frontend/src/core/models/types.ts b/frontend/src/core/models/types.ts
index 19404ae..0b9ea5a 100644
--- a/frontend/src/core/models/types.ts
+++ b/frontend/src/core/models/types.ts
@@ -1,6 +1,7 @@
export interface Model {
id: string;
name: string;
+ model: string;
display_name: string;
description?: string | null;
supports_thinking?: boolean;
diff --git a/frontend/src/core/threads/hooks.ts b/frontend/src/core/threads/hooks.ts
index 722bc9b..f32c5db 100644
--- a/frontend/src/core/threads/hooks.ts
+++ b/frontend/src/core/threads/hooks.ts
@@ -31,6 +31,29 @@ export type ThreadStreamOptions = {
onToolEnd?: (event: ToolEndEvent) => void;
};
+function getStreamErrorMessage(error: unknown): string {
+ if (typeof error === "string" && error.trim()) {
+ return error;
+ }
+ if (error instanceof Error && error.message.trim()) {
+ return error.message;
+ }
+ if (typeof error === "object" && error !== null) {
+ const message = Reflect.get(error, "message");
+ if (typeof message === "string" && message.trim()) {
+ return message;
+ }
+ const nestedError = Reflect.get(error, "error");
+ if (nestedError instanceof Error && nestedError.message.trim()) {
+ return nestedError.message;
+ }
+ if (typeof nestedError === "string" && nestedError.trim()) {
+ return nestedError;
+ }
+ }
+ return "Request failed.";
+}
+
export function useThreadStream({
threadId,
context,
@@ -148,6 +171,10 @@ export function useThreadStream({
updateSubtask({ id: e.task_id, latestMessage: e.message });
}
},
+ onError(error) {
+ setOptimisticMessages([]);
+ toast.error(getStreamErrorMessage(error));
+ },
onFinish(state) {
listeners.current.onFinish?.(state.values);
void queryClient.invalidateQueries({ queryKey: ["threads", "search"] });
diff --git a/frontend/src/styles/globals.css b/frontend/src/styles/globals.css
index bbe79d4..065d326 100644
--- a/frontend/src/styles/globals.css
+++ b/frontend/src/styles/globals.css
@@ -72,7 +72,7 @@
@theme {
--font-sans:
- var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif,
+ ui-sans-serif, system-ui, sans-serif,
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--animate-fade-in: fade-in 1.1s;