From a337c72753b2aba50a19613c751d15e315ad1cac Mon Sep 17 00:00:00 2001 From: Long Yixing Date: Tue, 14 Jul 2026 02:13:50 +0800 Subject: [PATCH] Fix Studio auto-titles for reasoning models (#7098) --- .../src/features/chat/runtime-provider.tsx | 9 +++-- tests/studio/test_chat_title_generation.py | 36 ++++++++++++++++--- 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/studio/frontend/src/features/chat/runtime-provider.tsx b/studio/frontend/src/features/chat/runtime-provider.tsx index b545695f9e..634e5ec8b0 100644 --- a/studio/frontend/src/features/chat/runtime-provider.tsx +++ b/studio/frontend/src/features/chat/runtime-provider.tsx @@ -77,6 +77,7 @@ const pendingRunStartReadyByMessageId = new Map>(); type TitleResponse = { choices?: Array<{ + finish_reason?: string | null; message?: { content?: string; }; @@ -474,6 +475,8 @@ async function generateTitleWithModel(payload: { max_tokens: 24, top_k: 20, repetition_penalty: 1.0, + enable_thinking: false, + reasoning_effort: "none", messages: [ { role: "system", @@ -489,8 +492,10 @@ async function generateTitleWithModel(payload: { .json() .catch(() => null)) as TitleResponse | null; if (!response.ok) return null; - const raw: string | undefined = body?.choices?.[0]?.message?.content; - if (!raw) return null; + const choice = body?.choices?.[0]; + if (choice?.finish_reason === "length") return null; + const raw: string | undefined = choice?.message?.content; + if (!raw || /<\/?think>/i.test(raw)) return null; return normalizeTitle(raw); } diff --git a/tests/studio/test_chat_title_generation.py b/tests/studio/test_chat_title_generation.py index 4f82b4ec03..b568a51400 100644 --- a/tests/studio/test_chat_title_generation.py +++ b/tests/studio/test_chat_title_generation.py @@ -65,6 +65,8 @@ def test_title_model_payload_includes_optional_assistant_reply(): assert "if (assistant)" in block assert "parts.push(`Assistant: ${assistant}`);" in block assert 'parts.join("\\n")' in block + assert "enable_thinking: false" in block + assert 'reasoning_effort: "none"' in block def test_generate_title_passes_first_assistant_reply_after_first_user(): @@ -81,6 +83,25 @@ def test_generate_title_passes_first_assistant_reply_after_first_user(): assert "assistantText," in block +def test_tool_call_only_first_assistant_still_uses_first_user_message(): + source = RUNTIME_TSX.read_text() + extract_block = " ".join(_balanced_block(source, "function extractTextParts").split()) + generate_block = " ".join(_balanced_block(source, "async generateTitle(remoteId").split()) + + assert ( + '.filter((p): p is Extract => p.type === "text")' + in extract_block + ) + assert ( + "const userText = extractTextParts(firstUser) || defaultTitle; const assistantText = extractTextParts(firstAssistant);" + in generate_block + ) + assert ( + "(await generateTitleWithModel({ userText, assistantText, })) || fallbackTitleFromUserText(userText);" + in generate_block + ) + + def test_auto_title_disabled_uses_deterministic_user_text_fallback(): block = _balanced_block( RUNTIME_TSX.read_text(), @@ -93,12 +114,18 @@ def test_auto_title_disabled_uses_deterministic_user_text_fallback(): def test_model_failure_still_falls_back_to_user_text(): - block = _balanced_block( - RUNTIME_TSX.read_text(), - "async generateTitle(remoteId", + source = RUNTIME_TSX.read_text() + model_block = _source_until( + source, + "async function generateTitleWithModel", + "\nconst inflightTitleByKey", ) + generate_block = _balanced_block(source, "async generateTitle(remoteId") - assert "})) || fallbackTitleFromUserText(userText);" in block + assert "finish_reason?: string | null;" in source + assert 'if (choice?.finish_reason === "length") return null;' in model_block + assert r"if (!raw || /<\/?think>/i.test(raw)) return null;" in model_block + assert "})) || fallbackTitleFromUserText(userText);" in generate_block def test_title_normalizer_still_enforces_output_constraints(): @@ -113,3 +140,4 @@ def test_title_normalizer_still_enforces_output_constraints(): assert 'replace(/[.!?:;,]+/g, " ")' in block assert 'title.split(" ").filter(Boolean).slice(0, 6)' in block assert "joined.length > 60" in block + assert "return normalizeTitle(raw);" in block