diff --git a/apps/web/__tests__/ai-diff-rules.test.ts b/apps/web/__tests__/ai-diff-rules.test.ts
index 403afe0aa8..c137168abe 100644
--- a/apps/web/__tests__/ai-diff-rules.test.ts
+++ b/apps/web/__tests__/ai-diff-rules.test.ts
@@ -2,7 +2,7 @@ import { describe, it, expect, vi } from "vitest";
import { aiDiffRules } from "@/utils/ai/rule/diff-rules";
import { getEmailAccount } from "@/__tests__/helpers";
-// pnpm test-ai ai-diff-rules
+// RUN_AI_TESTS=true pnpm test-ai ai-diff-rules
const isAiTest = process.env.RUN_AI_TESTS === "true";
@@ -33,13 +33,14 @@ describe.runIf(isAiTest)("aiDiffRules", () => {
});
expect(result).toEqual({
- addedRules: ['Label all emails from support@company.com as "Support"'],
+ addedRules: ['* Label all emails from support@company.com as "Support"'],
editedRules: [
{
- oldRule: `Archive all newsletters and label them "Newsletter"`,
- newRule: `Archive all newsletters and label them "Newsletter Updates"`,
+ oldRule: `* Archive all newsletters and label them "Newsletter"`,
+ newRule: `* Archive all newsletters and label them "Newsletter Updates"`,
},
],
+ removedRules: [`* Label receipts as "Receipt"`],
});
}, 15_000);
diff --git a/apps/web/package.json b/apps/web/package.json
index 9a9ef90d1a..f3b9876d0d 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -40,7 +40,7 @@
"@mux/mux-player-react": "3.4.0",
"@next/mdx": "15.3.3",
"@next/third-parties": "15.3.3",
- "@openrouter/ai-sdk-provider": "1.0.0-beta.6",
+ "@openrouter/ai-sdk-provider": "1.1.0",
"@portabletext/react": "3.2.1",
"@prisma/client": "6.6.0",
"@radix-ui/react-alert-dialog": "1.1.14",
diff --git a/apps/web/utils/ai/rule/diff-rules.ts b/apps/web/utils/ai/rule/diff-rules.ts
index bd99e1b40d..d0a8172f6b 100644
--- a/apps/web/utils/ai/rule/diff-rules.ts
+++ b/apps/web/utils/ai/rule/diff-rules.ts
@@ -2,20 +2,7 @@ import z from "zod";
import { createPatch } from "diff";
import type { EmailAccountWithAI } from "@/utils/llms/types";
import { getModel } from "@/utils/llms/model";
-import { createGenerateText } from "@/utils/llms";
-
-const inputSchema = z.object({
- addedRules: z.array(z.string()).describe("The added rules"),
- editedRules: z
- .array(
- z.object({
- oldRule: z.string().describe("The old rule"),
- newRule: z.string().describe("The new rule"),
- }),
- )
- .describe("The edited rules"),
- removedRules: z.array(z.string()).describe("The removed rules"),
-});
+import { createGenerateObject } from "@/utils/llms";
export async function aiDiffRules({
emailAccount,
@@ -53,11 +40,26 @@ Organize your response using the 'diff_rules' function.
IMPORTANT: Do not include a rule in more than one category. If a rule is edited, do not include it in the 'removedRules' category!
If a rule is edited, it is an edit and not a removal! Be extra careful to not make this mistake.
+
+Return the result in JSON format. Do not include any other text in your response.
+
+
+{
+ "addedRules": ["rule text1", "rule text2"],
+ "editedRules": [
+ {
+ "oldRule": "rule text3",
+ "newRule": "rule text4 updated"
+ },
+ ],
+ "removedRules": ["rule text5", "rule text6"]
+}
+
`;
const modelOptions = getModel(emailAccount.user, "chat");
- const generateObject = createGenerateText({
+ const generateObject = createGenerateObject({
userEmail: emailAccount.email,
label: "Diff rules",
modelOptions,
@@ -67,17 +69,22 @@ If a rule is edited, it is an edit and not a removal! Be extra careful to not ma
...modelOptions,
system,
prompt,
- tools: {
- diff_rules: {
- description:
- "Analyze two prompt files and their diff to return the differences",
- inputSchema,
- },
- },
+ schemaName: "diff_rules",
+ schemaDescription:
+ "The result of the diff rules analysis. Return the result in JSON format. Do not include any other text in your response.",
+ schema: z.object({
+ addedRules: z.array(z.string()).describe("The added rules"),
+ editedRules: z
+ .array(
+ z.object({
+ oldRule: z.string().describe("The old rule"),
+ newRule: z.string().describe("The new rule"),
+ }),
+ )
+ .describe("The edited rules"),
+ removedRules: z.array(z.string()).describe("The removed rules"),
+ }),
});
- const parsedRules = result.toolCalls?.[0]?.input as z.infer<
- typeof inputSchema
- >;
- return parsedRules;
+ return result.object;
}
diff --git a/apps/web/utils/llms/index.ts b/apps/web/utils/llms/index.ts
index 7d24bb04a5..3b25dc3d48 100644
--- a/apps/web/utils/llms/index.ts
+++ b/apps/web/utils/llms/index.ts
@@ -32,6 +32,8 @@ import { createScopedLogger } from "@/utils/logger";
const logger = createScopedLogger("llms");
+const MAX_LOG_LENGTH = 200;
+
const commonOptions: {
experimental_telemetry: { isEnabled: boolean };
headers?: Record;
@@ -53,8 +55,8 @@ export function createGenerateText({
const generate = async (model: LanguageModelV2) => {
logger.trace("Generating text", {
label,
- system: options.system?.slice(0, 200),
- prompt: options.prompt?.slice(0, 200),
+ system: options.system?.slice(0, MAX_LOG_LENGTH),
+ prompt: options.prompt?.slice(0, MAX_LOG_LENGTH),
});
const result = await generateText(
@@ -128,8 +130,8 @@ export function createGenerateObject({
logger.trace("Generating object", {
label,
- system: options.system?.slice(0, 200),
- prompt: options.prompt?.slice(0, 200),
+ system: options.system?.slice(0, MAX_LOG_LENGTH),
+ prompt: options.prompt?.slice(0, MAX_LOG_LENGTH),
});
const result = await generateObject(
diff --git a/biome.json b/biome.json
index fe61b28e3f..608a982c40 100644
--- a/biome.json
+++ b/biome.json
@@ -21,6 +21,7 @@
"useCollapsedElseIf": "off"
},
"suspicious": {
+ "noConsole": "warn",
"noExplicitAny": "off",
"noArrayIndexKey": "off",
"noEmptyBlockStatements": "off",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 278ca2fafc..c97e00633a 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -183,8 +183,8 @@ importers:
specifier: 15.3.3
version: 15.3.3(next@15.3.3(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)
'@openrouter/ai-sdk-provider':
- specifier: 1.0.0-beta.6
- version: 1.0.0-beta.6(ai@5.0.0(zod@3.25.46))(zod@3.25.46)
+ specifier: 1.1.0
+ version: 1.1.0(ai@5.0.0(zod@3.25.46))(zod@3.25.46)
'@portabletext/react':
specifier: 3.2.1
version: 3.2.1(react@19.1.0)
@@ -2656,11 +2656,11 @@ packages:
'@octokit/types@13.6.2':
resolution: {integrity: sha512-WpbZfZUcZU77DrSW4wbsSgTPfKcp286q3ItaIgvSbBpZJlu6mnYXAkjZz6LVZPXkEvLIM8McanyZejKTYUHipA==}
- '@openrouter/ai-sdk-provider@1.0.0-beta.6':
- resolution: {integrity: sha512-1cj8yUek4ib10MqZ+9fw1p1a3SR0Zhx3HH1J3+WQIXXuH/rUhiAkwovgPi1ADuC1QECPSPiwfhK2GecnDm8hGg==}
+ '@openrouter/ai-sdk-provider@1.1.0':
+ resolution: {integrity: sha512-e5cW/KbgGakHOOsDhnHI6a0IDul9ER5J4QGM4yN9EfQ8XHfOFgwGGpLOopoRwkqaX5UdyQrpzei+1DPzg95i0A==}
engines: {node: '>=18'}
peerDependencies:
- ai: ^5.0.0-beta.12
+ ai: ^5.0.0
zod: ^3.24.1 || ^v4
'@opentelemetry/api-logs@0.57.2':
@@ -13733,7 +13733,7 @@ snapshots:
'@octokit/openapi-types': 22.2.0
optional: true
- '@openrouter/ai-sdk-provider@1.0.0-beta.6(ai@5.0.0(zod@3.25.46))(zod@3.25.46)':
+ '@openrouter/ai-sdk-provider@1.1.0(ai@5.0.0(zod@3.25.46))(zod@3.25.46)':
dependencies:
ai: 5.0.0(zod@3.25.46)
zod: 3.25.46