diff --git a/src/consts.ts b/src/consts.ts index 18e7793..059e9d7 100644 --- a/src/consts.ts +++ b/src/consts.ts @@ -1,4 +1,8 @@ export const NEW_API_BASE_URL = "https://recoup-api.vercel.app"; export const RECOUP_API_KEY = process.env.RECOUP_API_KEY; export const CODING_AGENT_ACCOUNT_ID = "ccbed42f-4d91-4834-b954-2a64a77d8665"; -export const OPENCLAW_DEFAULT_MODEL = "vercel-ai-gateway/anthropic/claude-sonnet-4.6"; \ No newline at end of file +export const OPENCLAW_DEFAULT_MODEL = "vercel-ai-gateway/anthropic/claude-sonnet-4.6"; +// Scheduled customer tasks run headless with no user to catch a hallucinated report. +// Sonnet 5 respects the data-grounding rule (refuses to fabricate) where the prior +// default (haiku-4.5) did not — verified on the OneRPM/Apache benchmark (recoupable/chat#1833). +export const DEFAULT_TASK_MODEL = "anthropic/claude-sonnet-5"; diff --git a/src/tasks/__tests__/customerPromptTask.test.ts b/src/tasks/__tests__/customerPromptTask.test.ts new file mode 100644 index 0000000..677aa30 --- /dev/null +++ b/src/tasks/__tests__/customerPromptTask.test.ts @@ -0,0 +1,49 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; + +const mockRun = vi.hoisted(() => vi.fn()); +vi.mock("@trigger.dev/sdk/v3", () => ({ + logger: { log: vi.fn(), warn: vi.fn(), error: vi.fn() }, + tags: { add: vi.fn() }, + schedules: { + task: (config: { run: unknown }) => { + mockRun.mockImplementation(config.run as (...args: unknown[]) => unknown); + return config; + }, + }, +})); + +const mockFetchTask = vi.fn(); +vi.mock("../../recoup/fetchTask", () => ({ fetchTask: (...a: unknown[]) => mockFetchTask(...a) })); +const mockGetTaskRoomId = vi.fn(); +vi.mock("../../chats/getTaskRoomId", () => ({ getTaskRoomId: (...a: unknown[]) => mockGetTaskRoomId(...a) })); +const mockGenerateChat = vi.fn(); +vi.mock("../../recoup/generateChat", () => ({ generateChat: (...a: unknown[]) => mockGenerateChat(...a) })); + +// real chatSchema is used +import "../customerPromptTask"; + +const payload = { timestamp: new Date(0), timezone: "UTC", externalId: "ext-1" }; + +beforeEach(() => { + vi.clearAllMocks(); + mockGetTaskRoomId.mockResolvedValue("room-1"); + mockGenerateChat.mockResolvedValue(undefined); +}); + +describe("customerPromptTask model default", () => { + it("defaults the model to anthropic/claude-sonnet-5 when the task config omits it", async () => { + mockFetchTask.mockResolvedValue({ accountId: "acc-1", artistId: "art-1", prompt: "do a thing" }); + await mockRun(payload); + expect(mockGenerateChat).toHaveBeenCalledWith( + expect.objectContaining({ model: "anthropic/claude-sonnet-5", accountId: "acc-1" }), + ); + }); + + it("forwards an explicit task model unchanged", async () => { + mockFetchTask.mockResolvedValue({ accountId: "acc-1", model: "anthropic/claude-opus-4-8", prompt: "x" }); + await mockRun(payload); + expect(mockGenerateChat).toHaveBeenCalledWith( + expect.objectContaining({ model: "anthropic/claude-opus-4-8" }), + ); + }); +}); diff --git a/src/tasks/customerPromptTask.ts b/src/tasks/customerPromptTask.ts index 3768930..6f7e387 100644 --- a/src/tasks/customerPromptTask.ts +++ b/src/tasks/customerPromptTask.ts @@ -3,6 +3,7 @@ import { getTaskRoomId } from "../chats/getTaskRoomId"; import { fetchTask } from "../recoup/fetchTask"; import { generateChat } from "../recoup/generateChat"; import { chatSchema, type ChatConfig } from "../schemas/chatSchema"; +import { DEFAULT_TASK_MODEL } from "../consts"; type TaskPayload = { // Provided automatically by Trigger.dev schedules @@ -47,7 +48,7 @@ export const customerPromptTask = schedules.task({ const accountId = taskConfig?.accountId; const artistId = taskConfig?.artistId; - const model = taskConfig?.model; + const model = taskConfig?.model ?? DEFAULT_TASK_MODEL; const prompt = taskConfig?.prompt ?? "Draft a friendly check-in message for our customers.";