Skip to content

Commit f65b27e

Browse files
committed
feat(autumn): track workspace AI message usage
1 parent fe6d1cf commit f65b27e

8 files changed

Lines changed: 268 additions & 8 deletions

File tree

.dev.vars.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ BETTER_AUTH_URL="http://localhost:3000"
2727
# AI chat and title generation (Vercel AI Gateway).
2828
# AI_GATEWAY_API_KEY=""
2929

30+
# Autumn usage analytics. When unset, usage tracking is skipped.
31+
# AUTUMN_SECRET_KEY=""
32+
3033
# Web search and URL/PDF extraction (Firecrawl).
3134
# FIRECRAWL_API_KEY=""
3235

docs/ENVIRONMENT.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ The dev server boots and lets you sign in with just the two variables below. Eve
3737
| --- | --- | --- |
3838
| `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET` | Google sign-in | Google sign-in fails; use **Continue as guest** (shown in local dev) |
3939
| `AI_GATEWAY_API_KEY` | AI chat / title generation (Vercel AI Gateway) | AI calls error when invoked |
40+
| `AUTUMN_SECRET_KEY` | Autumn usage analytics for completed AI chat messages | Usage tracking is skipped |
4041
| `FIRECRAWL_API_KEY` | Web search + URL/PDF extraction (Firecrawl) | Those calls error when invoked |
4142
| `TCC_API_KEY` | Agent observability export (The Context Company) | AI chat still works; TCC export is skipped |
4243
| `LLAMA_CLOUD_API_KEY` | Document extraction (LlamaParse) | Those calls error when invoked |

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@
104104
"@tiptap/y-tiptap": "^3.0.5",
105105
"agents": "^0.17.0",
106106
"ai": "^6.0.208",
107+
"autumn-js": "^1.2.34",
107108
"better-auth": "^1.6.19",
108109
"class-variance-authority": "^0.7.1",
109110
"clsx": "^2.1.1",

pnpm-lock.yaml

Lines changed: 72 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/features/workspaces/ai/ai-thread.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { createCompactFunction } from "agents/experimental/memory/utils";
2020
import { generateText, type LanguageModel, type ToolSet } from "ai";
2121

2222
import type { AIInspectorSnapshot } from "#/features/workspaces/ai/ai-inspector";
23+
import type { AIThreadContext } from "#/features/workspaces/ai/ai-thread-metadata";
2324
import { AIThreadTelemetryRecorder } from "#/features/workspaces/ai/ai-thread-telemetry-recorder";
2425
import {
2526
createAIThreadTools,
@@ -34,8 +35,13 @@ import {
3435
DEFAULT_WORKSPACE_AI_CHAT_MODEL_ID,
3536
getWorkspaceAiChatModel,
3637
resolveWorkspaceAiChatModelId,
38+
type WorkspaceAiChatModelId,
3739
} from "#/features/workspaces/ai/models";
3840
import type { UserAIStore } from "#/features/workspaces/ai/user-ai-agents";
41+
import {
42+
checkWorkspaceAiMessageAccess,
43+
trackWorkspaceAiMessageUsage,
44+
} from "#/integrations/autumn/workspace-ai-usage";
3945

4046
const AI_THREAD_CHAT_RECOVERY_NO_PROGRESS_TIMEOUT_MS = 90_000;
4147
const AI_THREAD_CHAT_RECOVERY_TERMINAL_MESSAGE =
@@ -53,6 +59,11 @@ type AIThreadRunSettlement =
5359
kind: "failed";
5460
};
5561

62+
interface AIThreadUsageContext {
63+
modelId: WorkspaceAiChatModelId;
64+
thread: AIThreadContext;
65+
}
66+
5667
export function createAIThreadClass(getUserAIStore: () => typeof UserAIStore) {
5768
return class AIThread extends Think<Cloudflare.Env> {
5869
override chatRecovery = {
@@ -75,6 +86,7 @@ export function createAIThreadClass(getUserAIStore: () => typeof UserAIStore) {
7586
override sendReasoning = false;
7687
private shouldRefreshSessionPrompt = false;
7788
private activeRunStartedAt: number | undefined;
89+
private activeUsageContext: AIThreadUsageContext | undefined;
7890
private readonly telemetry = new AIThreadTelemetryRecorder({
7991
env: this.env,
8092
host: this,
@@ -153,6 +165,24 @@ export function createAIThreadClass(getUserAIStore: () => typeof UserAIStore) {
153165
}
154166

155167
const modelId = resolveWorkspaceAiChatModelId(ctx.body?.modelId);
168+
169+
if (!ctx.continuation) {
170+
const access = await checkWorkspaceAiMessageAccess({
171+
env: this.env,
172+
modelId,
173+
userId: thread.userId,
174+
});
175+
176+
if (!access.allowed) {
177+
throw new Error("Usage limit reached");
178+
}
179+
180+
this.activeUsageContext = {
181+
modelId,
182+
thread,
183+
};
184+
}
185+
156186
const system = getAIThreadSystemPromptForWorkspace(ctx.system, thread.promptScope, {
157187
timeZone: getBodyString(ctx.body, "timeZone"),
158188
workspaceAiContext: ctx.body?.workspaceAiContext,
@@ -204,6 +234,7 @@ export function createAIThreadClass(getUserAIStore: () => typeof UserAIStore) {
204234

205235
override async onChatResponse(result: ChatResponseResult) {
206236
this.telemetry.recordTurnFinished(result);
237+
this._trackCompletedMessageUsage(result);
207238
if (!this._shouldSettleRunAfterResponse(result)) {
208239
await this._refreshSessionPromptIfNeeded();
209240
return;
@@ -307,13 +338,35 @@ export function createAIThreadClass(getUserAIStore: () => typeof UserAIStore) {
307338
}
308339

309340
this.activeRunStartedAt = undefined;
341+
this.activeUsageContext = undefined;
310342
} catch (error) {
311343
onError(error);
312344
} finally {
313345
await this._refreshSessionPromptIfNeeded();
314346
}
315347
}
316348

349+
private _trackCompletedMessageUsage(result: ChatResponseResult) {
350+
if (result.continuation || result.status !== "completed") {
351+
return;
352+
}
353+
354+
const usageContext = this.activeUsageContext;
355+
if (!usageContext) {
356+
return;
357+
}
358+
359+
void this.keepAliveWhile(() =>
360+
trackWorkspaceAiMessageUsage({
361+
env: this.env,
362+
modelId: usageContext.modelId,
363+
threadId: usageContext.thread.id,
364+
userId: usageContext.thread.userId,
365+
workspaceId: usageContext.thread.workspaceId,
366+
}),
367+
);
368+
}
369+
317370
private async _handleChatRecoveryExhausted(ctx: ChatRecoveryExhaustedContext) {
318371
await this._recordAuxiliaryError({
319372
error: new Error(AI_THREAD_CHAT_RECOVERY_TERMINAL_MESSAGE),

0 commit comments

Comments
 (0)