diff --git a/extensions/discord/package.json b/extensions/discord/package.json index 324e157eb5b..3e0704d8e15 100644 --- a/extensions/discord/package.json +++ b/extensions/discord/package.json @@ -6,7 +6,6 @@ "dependencies": { "@buape/carbon": "0.0.0-beta-20260317045421", "@discordjs/voice": "^0.19.2", - "@mariozechner/pi-ai": "0.60.0", "discord-api-types": "^0.38.42", "https-proxy-agent": "^8.0.0", "opusscript": "^0.1.1" diff --git a/extensions/discord/src/monitor/thread-title.generate.test.ts b/extensions/discord/src/monitor/thread-title.generate.test.ts index 559c7e02b49..9c66ed7a138 100644 --- a/extensions/discord/src/monitor/thread-title.generate.test.ts +++ b/extensions/discord/src/monitor/thread-title.generate.test.ts @@ -2,19 +2,17 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; import { beforeEach, describe, expect, it, vi } from "vitest"; const hoisted = vi.hoisted(() => ({ - completeMock: vi.fn(), + completeWithPreparedSimpleCompletionModelMock: vi.fn(), prepareSimpleCompletionModelForAgentMock: vi.fn(), extractAssistantTextMock: vi.fn(), })); -vi.mock("@mariozechner/pi-ai", () => ({ - complete: hoisted.completeMock, -})); - vi.mock("openclaw/plugin-sdk/agent-runtime", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, + completeWithPreparedSimpleCompletionModel: + hoisted.completeWithPreparedSimpleCompletionModelMock, prepareSimpleCompletionModelForAgent: hoisted.prepareSimpleCompletionModelForAgentMock, extractAssistantText: hoisted.extractAssistantTextMock, }; @@ -24,7 +22,7 @@ let generateThreadTitle: typeof import("./thread-title.js").generateThreadTitle; beforeEach(async () => { vi.resetModules(); - hoisted.completeMock.mockReset(); + hoisted.completeWithPreparedSimpleCompletionModelMock.mockReset(); hoisted.prepareSimpleCompletionModelForAgentMock.mockReset(); hoisted.extractAssistantTextMock.mockReset(); @@ -44,7 +42,7 @@ beforeEach(async () => { mode: "api-key", }, }); - hoisted.completeMock.mockResolvedValue({}); + hoisted.completeWithPreparedSimpleCompletionModelMock.mockResolvedValue({}); hoisted.extractAssistantTextMock.mockReturnValue("Generated title"); ({ generateThreadTitle } = await import("./thread-title.js")); }); @@ -118,7 +116,7 @@ describe("generateThreadTitle", () => { }); expect(result).toBeNull(); - expect(hoisted.completeMock).not.toHaveBeenCalled(); + expect(hoisted.completeWithPreparedSimpleCompletionModelMock).not.toHaveBeenCalled(); }); it("returns null when shared completion prep fails", async () => { @@ -138,7 +136,7 @@ describe("generateThreadTitle", () => { }); expect(result).toBeNull(); - expect(hoisted.completeMock).not.toHaveBeenCalled(); + expect(hoisted.completeWithPreparedSimpleCompletionModelMock).not.toHaveBeenCalled(); }); it("builds contextual prompt and forwards completion options", async () => { @@ -151,8 +149,10 @@ describe("generateThreadTitle", () => { }); expect(result).toBe("Generated title"); - expect(hoisted.completeMock).toHaveBeenCalledTimes(1); - expect(hoisted.completeMock.mock.calls[0]?.[1]).toEqual( + expect(hoisted.completeWithPreparedSimpleCompletionModelMock).toHaveBeenCalledTimes(1); + expect( + hoisted.completeWithPreparedSimpleCompletionModelMock.mock.calls[0]?.[0]?.context, + ).toEqual( expect.objectContaining({ systemPrompt: "Generate a concise Discord thread title (3-6 words). Return only the title. Use channel context when provided and avoid redundant channel-name words unless needed for clarity.", @@ -164,10 +164,13 @@ describe("generateThreadTitle", () => { ], }), ); - expect(hoisted.completeMock.mock.calls[0]?.[1]?.messages?.[0]?.content).toContain( - "Channel description: Deploy updates and incident notes", - ); - expect(hoisted.completeMock.mock.calls[0]?.[2]).toEqual( + expect( + hoisted.completeWithPreparedSimpleCompletionModelMock.mock.calls[0]?.[0]?.context + ?.messages?.[0]?.content, + ).toContain("Channel description: Deploy updates and incident notes"); + expect( + hoisted.completeWithPreparedSimpleCompletionModelMock.mock.calls[0]?.[0]?.options, + ).toEqual( expect.objectContaining({ maxTokens: 24, temperature: 0.2, @@ -176,7 +179,9 @@ describe("generateThreadTitle", () => { }); it("returns null when completion throws", async () => { - hoisted.completeMock.mockRejectedValueOnce(new Error("network timeout")); + hoisted.completeWithPreparedSimpleCompletionModelMock.mockRejectedValueOnce( + new Error("network timeout"), + ); const result = await generateThreadTitle({ cfg: {} as OpenClawConfig, diff --git a/extensions/discord/src/monitor/thread-title.ts b/extensions/discord/src/monitor/thread-title.ts index 97cd1534f2d..ffb004dca59 100644 --- a/extensions/discord/src/monitor/thread-title.ts +++ b/extensions/discord/src/monitor/thread-title.ts @@ -1,5 +1,5 @@ -import { complete } from "@mariozechner/pi-ai"; import { + completeWithPreparedSimpleCompletionModel, extractAssistantText, prepareSimpleCompletionModelForAgent, } from "openclaw/plugin-sdk/agent-runtime"; @@ -15,8 +15,6 @@ const DISCORD_THREAD_TITLE_TEMPERATURE = 0.2; const DISCORD_THREAD_TITLE_SYSTEM_PROMPT = "Generate a concise Discord thread title (3-6 words). Return only the title. Use channel context when provided and avoid redundant channel-name words unless needed for clarity."; -type ThreadTitleModel = Parameters[0]; - export async function generateThreadTitle(params: { cfg: OpenClawConfig; agentId: string; @@ -55,7 +53,7 @@ export async function generateThreadTitle(params: { const timeoutMs = resolveThreadTitleTimeoutMs(params.timeoutMs); const response = await completeThreadTitle({ model: prepared.model, - apiKey: prepared.auth.apiKey, + auth: prepared.auth, userMessage, timeoutMs, }); @@ -68,17 +66,18 @@ export async function generateThreadTitle(params: { } async function completeThreadTitle(params: { - model: ThreadTitleModel; - apiKey: string | undefined; + model: Parameters[0]["model"]; + auth: Parameters[0]["auth"]; userMessage: string; timeoutMs: number; }) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), params.timeoutMs); try { - return await complete( - params.model, - { + return await completeWithPreparedSimpleCompletionModel({ + model: params.model, + auth: params.auth, + context: { systemPrompt: DISCORD_THREAD_TITLE_SYSTEM_PROMPT, messages: [ { @@ -88,13 +87,12 @@ async function completeThreadTitle(params: { }, ], }, - { - apiKey: params.apiKey, + options: { maxTokens: DISCORD_THREAD_TITLE_MAX_TOKENS, temperature: DISCORD_THREAD_TITLE_TEMPERATURE, signal: controller.signal, }, - ); + }); } finally { clearTimeout(timer); } diff --git a/src/agents/simple-completion-runtime.ts b/src/agents/simple-completion-runtime.ts index e4e9953eb7a..37c21117eb8 100644 --- a/src/agents/simple-completion-runtime.ts +++ b/src/agents/simple-completion-runtime.ts @@ -1,4 +1,4 @@ -import type { Api, Model } from "@mariozechner/pi-ai"; +import { complete, type Api, type Model } from "@mariozechner/pi-ai"; import type { OpenClawConfig } from "../config/config.js"; import { resolveAgentDir, resolveAgentEffectiveModelPrimary } from "./agent-scope.js"; import { DEFAULT_PROVIDER } from "./defaults.js"; @@ -231,3 +231,15 @@ export async function prepareSimpleCompletionModelForAgent(params: { auth: prepared.auth, }; } + +export async function completeWithPreparedSimpleCompletionModel(params: { + model: Model; + auth: ResolvedProviderAuth; + context: Parameters[1]; + options?: Omit[2], "apiKey">; +}) { + return await complete(params.model, params.context, { + ...params.options, + apiKey: params.auth.apiKey, + }); +}