fix(title): budget thinking models in AI session title generation (#1340) (#1382)

Co-authored-by: realLoganLuo <realLoganLuo@users.noreply.github.com>
Co-authored-by: 程序员阿江-Relakkes <relakkes@gmail.com>
This commit is contained in:
Logan-Luo
2026-09-27 18:57:01 +08:00
committed by GitHub
parent 76619c9c64
commit 462a8dfde5
2 changed files with 99 additions and 1 deletions
@@ -209,6 +209,99 @@ describe('titleService', () => {
}
})
test('budgets output tokens a thinking model can spend before the title (#1340)', async () => {
let requestBody: Record<string, unknown> | null = null
const server = Bun.serve({
hostname: '127.0.0.1',
port: 0,
async fetch(req) {
requestBody = await req.json() as Record<string, unknown>
return Response.json({
content: [{ type: 'text', text: '{"title":"Budget respected"}' }],
})
},
})
try {
const provider = await new ProviderService().addProvider({
presetId: 'custom', name: 'Thinking Budget', apiKey: 'test-key',
baseUrl: `http://127.0.0.1:${server.port}/anthropic`, apiFormat: 'anthropic',
models: {
main: 'glm-5.3-flash', haiku: 'glm-5.3-flash',
sonnet: 'glm-5.3-flash', opus: 'glm-5.3-flash',
},
})
await expect(generateTitle('给这条会话起个短标题', provider.id)).resolves.toBe('Budget respected')
// 100 tokens left nothing for the text once reasoning ran: empty content,
// null title, placeholder forever (#1340).
expect(requestBody?.max_tokens).toBeGreaterThanOrEqual(1024)
expect(requestBody?.thinking).toEqual({ type: 'disabled' })
} finally {
server.stop(true)
}
})
test('extracts the title when the response carries a thinking block first (#1340)', async () => {
const server = Bun.serve({
hostname: '127.0.0.1',
port: 0,
async fetch() {
return Response.json({
content: [
{ type: 'thinking', thinking: 'The user wants a short title about the migration.', signature: 'sig' },
{ type: 'text', text: '{"title":"Migration recap"}' },
],
})
},
})
try {
const provider = await new ProviderService().addProvider({
presetId: 'custom', name: 'Thinking Response', apiKey: 'test-key',
baseUrl: `http://127.0.0.1:${server.port}/anthropic`, apiFormat: 'anthropic',
models: {
main: 'glm-5.3-flash', haiku: 'glm-5.3-flash',
sonnet: 'glm-5.3-flash', opus: 'glm-5.3-flash',
},
})
await expect(generateTitle('Summarize the migration plan', provider.id))
.resolves.toBe('Migration recap')
} finally {
server.stop(true)
}
})
test('returns null when reasoning left no text block, so the caller keeps the placeholder (#1340)', async () => {
const server = Bun.serve({
hostname: '127.0.0.1',
port: 0,
async fetch() {
return Response.json({
content: [
{ type: 'thinking', thinking: 'burned the entire budget on reasoning', signature: 'sig' },
],
})
},
})
try {
const provider = await new ProviderService().addProvider({
presetId: 'custom', name: 'Starved Response', apiKey: 'test-key',
baseUrl: `http://127.0.0.1:${server.port}/anthropic`, apiFormat: 'anthropic',
models: {
main: 'glm-5.3-flash', haiku: 'glm-5.3-flash',
sonnet: 'glm-5.3-flash', opus: 'glm-5.3-flash',
},
})
await expect(generateTitle('Summarize the migration plan', provider.id)).resolves.toBeNull()
} finally {
server.stop(true)
}
})
test('derives slash-command titles from command metadata without raw XML tags', () => {
const raw = [
'<command-message>frontend-design</command-message>',
+6 -1
View File
@@ -31,7 +31,12 @@ import { extractConversationText, SESSION_TITLE_PROMPT } from '../../utils/sessi
import type { ProviderAuthStrategy } from '../types/provider.js'
const TITLE_MAX_LEN = 50
const TITLE_MAX_OUTPUT_TOKENS = 100
// Thinking models that ignore (or silently drop) `thinking: disabled` spend the
// whole budget on reasoning: the text block comes back empty, parsing yields
// null, and the session keeps the first-message placeholder forever (#1340).
// 100 was only enough for a title with no reasoning in front of it; 2048 leaves
// headroom for the thinking burn while staying trivial for a 3-7 word title.
const TITLE_MAX_OUTPUT_TOKENS = 2048
const TITLE_INPUT_MAX_LEN = 2000
export type TitleLanguagePreference = {