Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ NEXT_PUBLIC_APP_URL=http://localhost:3000
BETTER_AUTH_SECRET=your-better-auth-secret
BETTER_AUTH_URL=http://localhost:3000

# Autumn Billing
AUTUMN_SECRET_KEY=am_sk_test_...

# Google OAuth (for Better Auth)
GOOGLE_CLIENT_ID=...
GOOGLE_CLIENT_SECRET=...
Expand Down
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@
"ai": "^6.0.158",
"assemblyai": "^4.29.0",
"assistant-stream": "^0.3.10",
"autumn-js": "^1.2.9",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n package.json | sed -n '110,200p'

Repository: ThinkEx-OSS/thinkex

Length of output: 3182


🏁 Script executed:

fd -type f -name "*.patch" | grep autumn

Repository: ThinkEx-OSS/thinkex

Length of output: 233


🏁 Script executed:

rg "patchedDependencies" -A 10 package.json

Repository: ThinkEx-OSS/thinkex

Length of output: 160


🏁 Script executed:

find . -name "*autumn-js*" -type f 2>/dev/null

Repository: ThinkEx-OSS/thinkex

Length of output: 94


🌐 Web query:

pnpm patchedDependencies semver range version matching behavior

💡 Result:

pnpm's patchedDependencies field in pnpm-workspace.yaml or package.json uses semver ranges in keys to match installed package versions. For a given installed version of a package (e.g., foo@2.1.0), pnpm selects the patch using strict priority: 1. Exact version match (highest priority, e.g., "foo@2.1.0") 2. First matching version range (e.g., "foo@^2.0.0") 3. Name-only fallback (e.g., "foo") Exact matches override ranges. The highest priority matching entry is applied; lower ones are skipped for that version. Avoid overlapping ranges; use exclusions for sub-ranges (e.g., "foo@>=2.0.0 <2.2.0 || >2.8.0"). Example from docs: patchedDependencies: foo: patches/foo-1.patch # name-only foo@^2.0.0: patches/foo-2.patch # range foo@2.1.0: patches/foo-3.patch # exact For foo@2.1.0: foo-3.patch applies (exact). For foo@2.0.1: foo-2.patch (range, matches ^2.0.0, no exact). For foo@1.0.0: foo-1.patch (name-only). Special: "*" acts like name-only but fails install on patch error (unlike name-only). This uses standard semver range matching (via semver library). Feature added ~pnpm 10.13+ via PR #9236 (merged Mar 2025). Patch failure handling: - Exact/range: fails install by default (configurable via ignorePatchFailures/allowUnusedPatches). - Name-only: ignores failures by default.

Citations:


Pin autumn-js to the exact patched version.

Dependency uses ^1.2.9 (allows upgrades to 1.x.x), but patchedDependencies targets exact version 1.2.9. If the package resolves to 1.2.10 or 1.3.0, pnpm won't apply the patch and installation will silently proceed without it.

Proposed fix
-    "autumn-js": "^1.2.9",
+    "autumn-js": "1.2.9",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"autumn-js": "^1.2.9",
"autumn-js": "1.2.9",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` at line 116, The dependency "autumn-js" in package.json is
currently specified with a range "^1.2.9" which can resolve to newer 1.x
versions and bypass the patchedDependencies target; update the "autumn-js" entry
to the exact patched version "1.2.9" (remove the caret) so pnpm will match and
apply the patch exactly, then re-run installation to verify the patch is applied
for the package named "autumn-js".

"better-auth": "^1.6.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
Expand Down Expand Up @@ -185,5 +186,10 @@
"tw-animate-css": "^1.4.0",
"typescript": "^6.0.2",
"vitest": "^4.1.2"
},
"pnpm": {
"patchedDependencies": {
"autumn-js@1.2.9": "patches/autumn-js@1.2.9.patch"
}
}
}
13 changes: 13 additions & 0 deletions patches/autumn-js@1.2.9.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
diff --git a/dist/better-auth/chunk-LRULDM7S.mjs b/dist/better-auth/chunk-LRULDM7S.mjs
index 31334ee65452ccbf47388e1d4cd79ce7d1f908ce..4e24d0ed9013cd9968df667738d47aba98afd2e2 100644
--- a/dist/better-auth/chunk-LRULDM7S.mjs
+++ b/dist/better-auth/chunk-LRULDM7S.mjs
@@ -6,7 +6,7 @@ import {
} from "./chunk-GJAMWZNZ.mjs";

// src/better-auth/utils/createAutumnEndpoint.ts
-import { createAuthEndpoint } from "better-auth/plugins";
+import { createAuthEndpoint } from "better-auth/api";
var getRouteConfig = (routeName) => {
const route = routeConfigs.find((r) => r.route === routeName);
if (!route) throw new Error(`Route not found: ${routeName}`);
2 changes: 2 additions & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
patchedDependencies:
autumn-js@1.2.9: patches/autumn-js@1.2.9.patch
58 changes: 53 additions & 5 deletions src/app/api/chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
wrapLanguageModel,
} from "ai";
import { devToolsMiddleware } from "@ai-sdk/devtools";
import { Autumn } from "autumn-js";
import { withTracing } from "@posthog/ai";
import type { UIMessage } from "ai";
import { logger } from "@/lib/utils/logger";
Expand All @@ -21,13 +22,21 @@ import {
import { withServerObservability } from "@/lib/with-server-observability";
import { normalizeLegacyToolMessages } from "@/lib/ai/legacy-tool-message-compat";
import type { ReplySelection } from "@/lib/stores/ui-store";
import { getDefaultChatModelId, resolveGatewayModelId } from "@/lib/ai/models";
import {
getDefaultChatModelId,
isPremiumChatModel,
resolveGatewayModelId,
} from "@/lib/ai/models";
import {
buildGatewayProviderOptions,
createGatewayLanguageModel,
getGatewayAttributionHeaders,
} from "@/lib/ai/gateway-provider-options";

const autumn = new Autumn({
secretKey: process.env.AUTUMN_SECRET_KEY!,
});

/**
* Extract workspaceId from system context or request body
*/
Expand Down Expand Up @@ -126,6 +135,7 @@ async function handlePOST(req: Request) {

// Get authenticated user ID
const session = await auth.api.getSession({ headers: headersObj });
const isAnonymousUser = Boolean(session?.user?.isAnonymous);
userId = session?.user?.id || null;

const { messages = [] }: { messages?: UIMessage[] } = body;
Expand Down Expand Up @@ -188,9 +198,8 @@ async function handlePOST(req: Request) {
// Get pre-formatted selected cards context from client (no DB fetch needed)
const selectedCardsContext = getSelectedCardsContext(body);

const modelId = resolveGatewayModelId(
body.modelId || getDefaultChatModelId(),
);
const rawModelId = body.modelId || getDefaultChatModelId();
const modelId = resolveGatewayModelId(rawModelId);

// Inject selected cards + reply selections into the last user message
injectSelectionContext(
Expand All @@ -199,6 +208,33 @@ async function handlePOST(req: Request) {
selectedCardsContext,
);

if (userId && !isAnonymousUser && isPremiumChatModel(rawModelId)) {
try {
const checkResult = await autumn.check({
customerId: userId,
featureId: "premium_message",
requiredBalance: 1,
});

if (!checkResult.allowed) {
return new Response(
JSON.stringify({
error: "credits_exhausted",
message:
"You've used all your premium AI messages for this month. Upgrade to Pro for unlimited access, or switch to another model.",
code: "CREDITS_EXHAUSTED",
}),
{
status: 403,
headers: { "Content-Type": "application/json" },
},
);
}
} catch (err) {
console.error("[billing] Autumn check failed, allowing request:", err);
}
}

const posthogClient = getPostHogServerClient();
const baseGatewayModel = createGatewayLanguageModel(modelId);
const tracedModel = posthogClient
Expand Down Expand Up @@ -245,7 +281,7 @@ async function handlePOST(req: Request) {
},
},
experimental_transform: smoothStream({ chunking: "word", delayInMs: 15 }),
onFinish: ({ usage, finishReason }) => {
onFinish: async ({ usage, finishReason }) => {
const usageInfo = {
inputTokens: usage?.inputTokens,
outputTokens: usage?.outputTokens,
Expand All @@ -257,6 +293,18 @@ async function handlePOST(req: Request) {
};

logger.info("📊 [CHAT-API] Final Token Usage:", usageInfo);

if (userId && !isAnonymousUser && isPremiumChatModel(rawModelId)) {
try {
await autumn.track({
customerId: userId!,
featureId: "premium_message",
value: 1,
});
} catch (err) {
console.error("[billing] Autumn track failed:", err);
}
}
},
onStepFinish: (result) => {
// stepType exists in runtime but may not be in type definitions
Expand Down
52 changes: 52 additions & 0 deletions src/app/billing/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"use client";

import { useEffect } from "react";
import { useRouter } from "next/navigation";
import { AnonymousSessionHandler } from "@/components/layout/SessionHandler";
import { Skeleton } from "@/components/ui/skeleton";
import { useSession } from "@/lib/auth-client";

function BillingAccessGate({ children }: { children: React.ReactNode }) {
const { data: session, isPending } = useSession();
const router = useRouter();

useEffect(() => {
if (!isPending && (!session || session.user?.isAnonymous)) {
router.replace("/auth/sign-up?redirect_url=%2Fbilling");
}
}, [isPending, router, session]);

if (isPending || !session || session.user?.isAnonymous) {
return (
<main className="mx-auto flex min-h-screen w-full max-w-4xl flex-col gap-8 px-4 py-12 md:px-6">
<div className="space-y-3 text-center">
<Skeleton className="mx-auto h-10 w-56" />
<Skeleton className="mx-auto h-4 w-72" />
</div>
<div className="space-y-4 rounded-2xl border bg-card p-6">
<Skeleton className="h-7 w-32" />
<Skeleton className="h-4 w-56" />
<Skeleton className="h-2 w-full" />
</div>
<div className="grid gap-4 md:grid-cols-2">
<Skeleton className="h-64 rounded-2xl" />
<Skeleton className="h-64 rounded-2xl" />
</div>
</main>
);
}

return <>{children}</>;
}

export default function BillingLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<AnonymousSessionHandler>
<BillingAccessGate>{children}</BillingAccessGate>
</AnonymousSessionHandler>
);
}
Loading
Loading