fix: make conversation labels work with Codex

This commit is contained in:
Clever
2026-05-06 14:29:36 +03:00
parent 14a113f7e5
commit 994209eb56
2 changed files with 92 additions and 2 deletions

View File

@@ -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",
);
});
});

View File

@@ -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<string | null> {
@@ -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)