fix(chat): gemini empty-answer fallback + empty-completion guard; chat routes accept workspace key
This commit is contained in:
@@ -397,9 +397,9 @@ export async function POST(request: Request) {
|
||||
const principal = await requireWorkspacePrincipal(request);
|
||||
if (principal instanceof NextResponse) return principal;
|
||||
|
||||
const userRow = await queryOne<{ data: any }>(
|
||||
const userRow = await queryOne<{ data: { email?: string } }>(
|
||||
`SELECT data FROM fs_users WHERE id = $1 LIMIT 1`,
|
||||
[principal.userId]
|
||||
[principal.userId],
|
||||
);
|
||||
if (!userRow?.data?.email) {
|
||||
return NextResponse.json({ error: "Unauthorized user" }, { status: 401 });
|
||||
@@ -1056,6 +1056,25 @@ export async function POST(request: Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// Last-resort guard: the model produced NO user-facing text and NO
|
||||
// tools (e.g. a "thinking" turn that returned only reasoning with an
|
||||
// empty answer part). The tool-tray recovery above doesn't cover this
|
||||
// case, so without this the user gets a silent blank bubble. Emit a
|
||||
// short deterministic fallback so every turn says *something*.
|
||||
if (
|
||||
!aborted &&
|
||||
assistantText.trim().length === 0 &&
|
||||
!anyToolsExecuted
|
||||
) {
|
||||
const fallback =
|
||||
"I didn't produce a response for that — I may have spent the turn " +
|
||||
"reasoning without writing an answer. Could you rephrase or add a " +
|
||||
"bit more detail?";
|
||||
assistantText = fallback;
|
||||
assistantTextSegments.push(fallback);
|
||||
emit({ type: "text", text: fallback });
|
||||
}
|
||||
|
||||
// Persist final assistant message. We include `textSegments`
|
||||
// alongside the legacy concatenated `content` so the client
|
||||
// can render reloaded threads with the same per-round bubble
|
||||
|
||||
@@ -11,7 +11,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ id:
|
||||
const principal = await requireWorkspacePrincipal(request);
|
||||
if (principal instanceof NextResponse) return principal;
|
||||
|
||||
const userRow = await queryOne<{ data: any }>(
|
||||
const userRow = await queryOne<{ data: { email?: string } }>(
|
||||
`SELECT data FROM fs_users WHERE id = $1 LIMIT 1`,
|
||||
[principal.userId]
|
||||
);
|
||||
@@ -42,7 +42,7 @@ export async function PATCH(request: Request, { params }: { params: Promise<{ id
|
||||
const principal = await requireWorkspacePrincipal(request);
|
||||
if (principal instanceof NextResponse) return principal;
|
||||
|
||||
const userRow = await queryOne<{ data: any }>(
|
||||
const userRow = await queryOne<{ data: { email?: string } }>(
|
||||
`SELECT data FROM fs_users WHERE id = $1 LIMIT 1`,
|
||||
[principal.userId]
|
||||
);
|
||||
@@ -64,7 +64,7 @@ export async function DELETE(request: Request, { params }: { params: Promise<{ i
|
||||
const principal = await requireWorkspacePrincipal(request);
|
||||
if (principal instanceof NextResponse) return principal;
|
||||
|
||||
const userRow = await queryOne<{ data: any }>(
|
||||
const userRow = await queryOne<{ data: { email?: string } }>(
|
||||
`SELECT data FROM fs_users WHERE id = $1 LIMIT 1`,
|
||||
[principal.userId]
|
||||
);
|
||||
|
||||
@@ -57,7 +57,7 @@ export async function GET(request: Request) {
|
||||
const principal = await requireWorkspacePrincipal(request);
|
||||
if (principal instanceof NextResponse) return principal;
|
||||
|
||||
const userRow = await queryOne<{ data: any }>(
|
||||
const userRow = await queryOne<{ data: { email?: string } }>(
|
||||
`SELECT data FROM fs_users WHERE id = $1 LIMIT 1`,
|
||||
[principal.userId]
|
||||
);
|
||||
@@ -120,7 +120,7 @@ export async function POST(request: Request) {
|
||||
const principal = await requireWorkspacePrincipal(request);
|
||||
if (principal instanceof NextResponse) return principal;
|
||||
|
||||
const userRow = await queryOne<{ data: any }>(
|
||||
const userRow = await queryOne<{ data: { email?: string } }>(
|
||||
`SELECT data FROM fs_users WHERE id = $1 LIMIT 1`,
|
||||
[principal.userId]
|
||||
);
|
||||
|
||||
@@ -155,6 +155,15 @@ export async function callGeminiChat(opts: {
|
||||
}
|
||||
}
|
||||
|
||||
// Empty-answer fallback: Gemini "thinking" responses can spend the whole
|
||||
// turn emitting `thought` parts and return NO answer part — leaving `text`
|
||||
// empty. Mirror the OpenAI-compatible adapter (which promotes
|
||||
// reasoning_content when content is empty) so the user never gets a blank
|
||||
// bubble. Only promote when there's also no tool call to render.
|
||||
if (!text.trim() && thoughts.trim() && toolCalls.length === 0) {
|
||||
text = thoughts.trim();
|
||||
}
|
||||
|
||||
return {
|
||||
text,
|
||||
thoughts,
|
||||
|
||||
Reference in New Issue
Block a user