free tier
This commit is contained in:
parent
bbc2739832
commit
ee2d8bcb6c
4 changed files with 108 additions and 14 deletions
|
|
@ -17,6 +17,7 @@ export default defineSchema({
|
||||||
user_settings: defineTable({
|
user_settings: defineTable({
|
||||||
user_id: v.string(),
|
user_id: v.string(),
|
||||||
privacy_mode: v.boolean(),
|
privacy_mode: v.boolean(),
|
||||||
|
free_messages_used: v.optional(v.number()),
|
||||||
}).index('by_user', ['user_id']),
|
}).index('by_user', ['user_id']),
|
||||||
user_keys: defineTable({
|
user_keys: defineTable({
|
||||||
user_id: v.string(),
|
user_id: v.string(),
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,41 @@ export const get = query({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const incrementFreeMessageCount = mutation({
|
||||||
|
args: {
|
||||||
|
session_token: v.string(),
|
||||||
|
},
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
const session = await ctx.runQuery(internal.betterAuth.getSession, {
|
||||||
|
sessionToken: args.session_token,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
throw new Error('Invalid session token');
|
||||||
|
}
|
||||||
|
|
||||||
|
const s = session as SessionObj;
|
||||||
|
|
||||||
|
const existing = await ctx.db
|
||||||
|
.query('user_settings')
|
||||||
|
.withIndex('by_user', (q) => q.eq('user_id', s.userId))
|
||||||
|
.first();
|
||||||
|
|
||||||
|
if (!existing) {
|
||||||
|
await ctx.db.insert('user_settings', {
|
||||||
|
user_id: s.userId,
|
||||||
|
privacy_mode: false,
|
||||||
|
free_messages_used: 1,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const currentCount = existing.free_messages_used || 0;
|
||||||
|
await ctx.db.patch(existing._id, {
|
||||||
|
free_messages_used: currentCount + 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export const set = mutation({
|
export const set = mutation({
|
||||||
args: {
|
args: {
|
||||||
privacy_mode: v.boolean(),
|
privacy_mode: v.boolean(),
|
||||||
|
|
@ -51,6 +86,7 @@ export const set = mutation({
|
||||||
await ctx.db.insert('user_settings', {
|
await ctx.db.insert('user_settings', {
|
||||||
user_id: s.userId,
|
user_id: s.userId,
|
||||||
privacy_mode: args.privacy_mode,
|
privacy_mode: args.privacy_mode,
|
||||||
|
free_messages_used: 0,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await ctx.db.patch(existing._id, {
|
await ctx.db.patch(existing._id, {
|
||||||
|
|
@ -69,6 +105,7 @@ export const create = mutation({
|
||||||
await ctx.db.insert('user_settings', {
|
await ctx.db.insert('user_settings', {
|
||||||
user_id: args.user_id,
|
user_id: args.user_id,
|
||||||
privacy_mode: false,
|
privacy_mode: false,
|
||||||
|
free_messages_used: 0,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { PUBLIC_CONVEX_URL } from '$env/static/public';
|
import { PUBLIC_CONVEX_URL } from '$env/static/public';
|
||||||
|
import { OPENROUTER_FREE_KEY } from '$env/static/private';
|
||||||
import { api } from '$lib/backend/convex/_generated/api';
|
import { api } from '$lib/backend/convex/_generated/api';
|
||||||
import type { Doc, Id } from '$lib/backend/convex/_generated/dataModel';
|
import type { Doc, Id } from '$lib/backend/convex/_generated/dataModel';
|
||||||
import { Provider } from '$lib/types';
|
import { Provider } from '$lib/types';
|
||||||
|
|
@ -74,11 +75,10 @@ async function generateConversationTitle({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const key = keyResult.value;
|
const userKey = keyResult.value;
|
||||||
if (!key) {
|
const actualKey = userKey || OPENROUTER_FREE_KEY;
|
||||||
log('Title generation: No API key found', startTime);
|
|
||||||
return;
|
log(`Title generation: Using ${userKey ? 'user' : 'free tier'} API key`, startTime);
|
||||||
}
|
|
||||||
|
|
||||||
// Only generate title if conversation currently has default title
|
// Only generate title if conversation currently has default title
|
||||||
const conversationResult = await ResultAsync.fromPromise(
|
const conversationResult = await ResultAsync.fromPromise(
|
||||||
|
|
@ -103,7 +103,7 @@ async function generateConversationTitle({
|
||||||
|
|
||||||
const openai = new OpenAI({
|
const openai = new OpenAI({
|
||||||
baseURL: 'https://openrouter.ai/api/v1',
|
baseURL: 'https://openrouter.ai/api/v1',
|
||||||
apiKey: key,
|
apiKey: actualKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create a prompt for title generation using only the first user message
|
// Create a prompt for title generation using only the first user message
|
||||||
|
|
@ -164,6 +164,7 @@ async function generateAIResponse({
|
||||||
modelResultPromise,
|
modelResultPromise,
|
||||||
keyResultPromise,
|
keyResultPromise,
|
||||||
rulesResultPromise,
|
rulesResultPromise,
|
||||||
|
userSettingsPromise,
|
||||||
abortSignal,
|
abortSignal,
|
||||||
}: {
|
}: {
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
|
|
@ -172,6 +173,7 @@ async function generateAIResponse({
|
||||||
keyResultPromise: ResultAsync<string | null, string>;
|
keyResultPromise: ResultAsync<string | null, string>;
|
||||||
modelResultPromise: ResultAsync<Doc<'user_enabled_models'> | null, string>;
|
modelResultPromise: ResultAsync<Doc<'user_enabled_models'> | null, string>;
|
||||||
rulesResultPromise: ResultAsync<Doc<'user_rules'>[], string>;
|
rulesResultPromise: ResultAsync<Doc<'user_rules'>[], string>;
|
||||||
|
userSettingsPromise: ResultAsync<Doc<'user_settings'> | null, string>;
|
||||||
abortSignal?: AbortSignal;
|
abortSignal?: AbortSignal;
|
||||||
}) {
|
}) {
|
||||||
log('Starting AI response generation in background', startTime);
|
log('Starting AI response generation in background', startTime);
|
||||||
|
|
@ -181,7 +183,7 @@ async function generateAIResponse({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [modelResult, keyResult, messagesQueryResult, rulesResult] = await Promise.all([
|
const [modelResult, keyResult, messagesQueryResult, rulesResult, userSettingsResult] = await Promise.all([
|
||||||
modelResultPromise,
|
modelResultPromise,
|
||||||
keyResultPromise,
|
keyResultPromise,
|
||||||
ResultAsync.fromPromise(
|
ResultAsync.fromPromise(
|
||||||
|
|
@ -192,6 +194,7 @@ async function generateAIResponse({
|
||||||
(e) => `Failed to get messages: ${e}`
|
(e) => `Failed to get messages: ${e}`
|
||||||
),
|
),
|
||||||
rulesResultPromise,
|
rulesResultPromise,
|
||||||
|
userSettingsPromise,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (modelResult.isErr()) {
|
if (modelResult.isErr()) {
|
||||||
|
|
@ -278,10 +281,9 @@ async function generateAIResponse({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const key = keyResult.value;
|
if (userSettingsResult.isErr()) {
|
||||||
if (!key) {
|
|
||||||
handleGenerationError({
|
handleGenerationError({
|
||||||
error: 'No API key found',
|
error: `User settings query failed: ${userSettingsResult.error}`,
|
||||||
conversationId,
|
conversationId,
|
||||||
messageId: mid,
|
messageId: mid,
|
||||||
sessionToken,
|
sessionToken,
|
||||||
|
|
@ -290,7 +292,52 @@ async function generateAIResponse({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
log('Background: API key retrieved successfully', startTime);
|
const userKey = keyResult.value;
|
||||||
|
const userSettings = userSettingsResult.value;
|
||||||
|
let actualKey: string;
|
||||||
|
|
||||||
|
if (userKey) {
|
||||||
|
// User has their own API key
|
||||||
|
actualKey = userKey;
|
||||||
|
log('Background: Using user API key', startTime);
|
||||||
|
} else {
|
||||||
|
// User doesn't have API key, check free tier limit
|
||||||
|
const freeMessagesUsed = userSettings?.free_messages_used || 0;
|
||||||
|
|
||||||
|
if (freeMessagesUsed >= 10) {
|
||||||
|
handleGenerationError({
|
||||||
|
error: 'Free message limit reached (10/10). Please add your own OpenRouter API key to continue chatting.',
|
||||||
|
conversationId,
|
||||||
|
messageId: mid,
|
||||||
|
sessionToken,
|
||||||
|
startTime,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increment free message count before generating
|
||||||
|
const incrementResult = await ResultAsync.fromPromise(
|
||||||
|
client.mutation(api.user_settings.incrementFreeMessageCount, {
|
||||||
|
session_token: sessionToken,
|
||||||
|
}),
|
||||||
|
(e) => `Failed to increment free message count: ${e}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (incrementResult.isErr()) {
|
||||||
|
handleGenerationError({
|
||||||
|
error: `Failed to track free message usage: ${incrementResult.error}`,
|
||||||
|
conversationId,
|
||||||
|
messageId: mid,
|
||||||
|
sessionToken,
|
||||||
|
startTime,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use environment OpenRouter key
|
||||||
|
actualKey = OPENROUTER_FREE_KEY;
|
||||||
|
log(`Background: Using free tier (${freeMessagesUsed + 1}/10 messages)`, startTime);
|
||||||
|
}
|
||||||
|
|
||||||
if (rulesResult.isErr()) {
|
if (rulesResult.isErr()) {
|
||||||
handleGenerationError({
|
handleGenerationError({
|
||||||
|
|
@ -328,7 +375,7 @@ async function generateAIResponse({
|
||||||
|
|
||||||
const openai = new OpenAI({
|
const openai = new OpenAI({
|
||||||
baseURL: 'https://openrouter.ai/api/v1',
|
baseURL: 'https://openrouter.ai/api/v1',
|
||||||
apiKey: key,
|
apiKey: actualKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
const formattedMessages = messages.map((m) => {
|
const formattedMessages = messages.map((m) => {
|
||||||
|
|
@ -453,7 +500,7 @@ ${attachedRules.map((r) => `- ${r.name}: ${r.rule}`).join('\n')}`,
|
||||||
(e) => `Failed to render HTML: ${e}`
|
(e) => `Failed to render HTML: ${e}`
|
||||||
);
|
);
|
||||||
|
|
||||||
const generationStatsResult = await retryResult(() => getGenerationStats(generationId!, key), {
|
const generationStatsResult = await retryResult(() => getGenerationStats(generationId!, actualKey), {
|
||||||
delay: 500,
|
delay: 500,
|
||||||
retries: 2,
|
retries: 2,
|
||||||
startTime,
|
startTime,
|
||||||
|
|
@ -594,6 +641,13 @@ export const POST: RequestHandler = async ({ request }) => {
|
||||||
(e) => `Failed to get API key: ${e}`
|
(e) => `Failed to get API key: ${e}`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const userSettingsPromise = ResultAsync.fromPromise(
|
||||||
|
client.query(api.user_settings.get, {
|
||||||
|
session_token: sessionToken,
|
||||||
|
}),
|
||||||
|
(e) => `Failed to get user settings: ${e}`
|
||||||
|
);
|
||||||
|
|
||||||
const rulesResultPromise = ResultAsync.fromPromise(
|
const rulesResultPromise = ResultAsync.fromPromise(
|
||||||
client.query(api.user_rules.all, {
|
client.query(api.user_rules.all, {
|
||||||
session_token: sessionToken,
|
session_token: sessionToken,
|
||||||
|
|
@ -688,6 +742,7 @@ export const POST: RequestHandler = async ({ request }) => {
|
||||||
modelResultPromise,
|
modelResultPromise,
|
||||||
keyResultPromise,
|
keyResultPromise,
|
||||||
rulesResultPromise,
|
rulesResultPromise,
|
||||||
|
userSettingsPromise,
|
||||||
abortSignal: abortController.signal,
|
abortSignal: abortController.signal,
|
||||||
})
|
})
|
||||||
.catch(async (error) => {
|
.catch(async (error) => {
|
||||||
|
|
|
||||||
|
|
@ -135,7 +135,8 @@
|
||||||
>!
|
>!
|
||||||
</h2>
|
</h2>
|
||||||
<p class="mt-2 text-left text-lg">
|
<p class="mt-2 text-left text-lg">
|
||||||
You need to provide a key to start sending messages.
|
You can send some free messages, or provide a key for limitless access.
|
||||||
|
<br />
|
||||||
<a href="/account/api-keys" class="text-primary"> Go to settings </a>
|
<a href="/account/api-keys" class="text-primary"> Go to settings </a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue