From 994209eb5679bff1a812f47273539261538b9b3f Mon Sep 17 00:00:00 2001 From: Clever Date: Wed, 6 May 2026 14:29:36 +0300 Subject: [PATCH] fix: make conversation labels work with Codex --- .../conversation-label-generator.test.ts | 70 +++++++++++++++++++ .../reply/conversation-label-generator.ts | 24 ++++++- 2 files changed, 92 insertions(+), 2 deletions(-) diff --git a/src/auto-reply/reply/conversation-label-generator.test.ts b/src/auto-reply/reply/conversation-label-generator.test.ts index 7ca2748bd59..0671181b70c 100644 --- a/src/auto-reply/reply/conversation-label-generator.test.ts +++ b/src/auto-reply/reply/conversation-label-generator.test.ts @@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; const completeSimple = vi.hoisted(() => vi.fn()); const getRuntimeAuthForModel = vi.hoisted(() => vi.fn()); +const logVerbose = vi.hoisted(() => vi.fn()); const requireApiKey = vi.hoisted(() => vi.fn()); const resolveDefaultModelForAgent = vi.hoisted(() => vi.fn()); const resolveModelAsync = vi.hoisted(() => vi.fn()); @@ -18,6 +19,8 @@ vi.mock("@mariozechner/pi-ai", async () => { vi.mock("../../agents/model-auth.js", () => ({ requireApiKey })); +vi.mock("../../globals.js", () => ({ logVerbose })); + vi.mock("../../agents/model-selection.js", () => ({ resolveDefaultModelForAgent, })); @@ -40,6 +43,7 @@ describe("generateConversationLabel", () => { beforeEach(() => { completeSimple.mockReset(); getRuntimeAuthForModel.mockReset(); + logVerbose.mockReset(); requireApiKey.mockReset(); resolveDefaultModelForAgent.mockReset(); resolveModelAsync.mockReset(); @@ -88,4 +92,70 @@ describe("generateConversationLabel", () => { cfg: {}, }); }); + + it("passes the label prompt as systemPrompt and the user text as message content", async () => { + await generateConversationLabel({ + userMessage: "Need help with invoices", + prompt: "Generate a label", + cfg: {}, + }); + + expect(completeSimple).toHaveBeenCalledWith( + { provider: "openai" }, + { + systemPrompt: "Generate a label", + messages: [ + { + role: "user", + content: "Need help with invoices", + timestamp: expect.any(Number), + }, + ], + }, + expect.objectContaining({ + apiKey: "resolved-key", + maxTokens: 100, + temperature: 0.3, + signal: expect.any(AbortSignal), + }), + ); + }); + + it("omits temperature for Codex Responses simple completions", async () => { + resolveDefaultModelForAgent.mockReturnValue({ provider: "openai-codex", model: "gpt-5.5" }); + resolveModelAsync.mockResolvedValue({ + model: { provider: "openai-codex", api: "openai-codex-responses" }, + authStorage: {}, + modelRegistry: {}, + }); + + await generateConversationLabel({ + userMessage: "тест создания топика-треда", + prompt: "Generate a label", + cfg: {}, + }); + + expect(completeSimple.mock.calls[0]?.[2]).toEqual( + expect.not.objectContaining({ temperature: expect.anything() }), + ); + }); + + it("logs completion errors instead of treating them as empty labels", async () => { + completeSimple.mockResolvedValue({ + content: [], + stopReason: "error", + errorMessage: "Codex error: Instructions are required", + }); + + const label = await generateConversationLabel({ + userMessage: "Need help with invoices", + prompt: "Generate a label", + cfg: {}, + }); + + expect(label).toBeNull(); + expect(logVerbose).toHaveBeenCalledWith( + "conversation-label-generator: completion failed: Codex error: Instructions are required", + ); + }); }); diff --git a/src/auto-reply/reply/conversation-label-generator.ts b/src/auto-reply/reply/conversation-label-generator.ts index 63057c7b6e0..b84dc726b40 100644 --- a/src/auto-reply/reply/conversation-label-generator.ts +++ b/src/auto-reply/reply/conversation-label-generator.ts @@ -23,6 +23,20 @@ function isTextContentBlock(block: { type: string }): block is TextContent { return block.type === "text"; } +function isCodexSimpleCompletionModel(model: { api?: string; provider?: string }): boolean { + return model.provider === "openai-codex" || model.api === "openai-codex-responses"; +} + +function extractSimpleCompletionError(result: { + stopReason?: string; + errorMessage?: string; +}): string | null { + if (result.stopReason !== "error") { + return null; + } + return result.errorMessage?.trim() || "unknown error"; +} + export async function generateConversationLabel( params: ConversationLabelParams, ): Promise { @@ -58,10 +72,11 @@ export async function generateConversationLabel( const result = await completeSimple( completionModel, { + systemPrompt: prompt, messages: [ { role: "user", - content: `${prompt}\n\n${userMessage}`, + content: userMessage, timestamp: Date.now(), }, ], @@ -69,10 +84,15 @@ export async function generateConversationLabel( { apiKey, maxTokens: 100, - temperature: 0.3, + ...(isCodexSimpleCompletionModel(completionModel) ? {} : { temperature: 0.3 }), signal: controller.signal, }, ); + const errorMessage = extractSimpleCompletionError(result); + if (errorMessage) { + logVerbose(`conversation-label-generator: completion failed: ${errorMessage}`); + return null; + } const text = result.content .filter(isTextContentBlock)