75 lines
2.0 KiB
TypeScript
75 lines
2.0 KiB
TypeScript
'use server';
|
|
|
|
import { streamText } from 'ai';
|
|
import { createStreamableValue } from 'ai/rsc';
|
|
import { createOpenAI } from '@ai-sdk/openai';
|
|
import { kv } from '@vercel/kv';
|
|
import { Ratelimit } from '@upstash/ratelimit';
|
|
import { AI_TEXT_GENERATION_ENABLED, HAS_VERCEL_KV } from '@/site/config';
|
|
import { safelyRunAdminServerAction } from '@/auth';
|
|
import { removeBase64Prefix } from '@/utility/image';
|
|
|
|
const RATE_LIMIT_IDENTIFIER = 'openai-image-query';
|
|
const RATE_LIMIT_MAX_QUERIES_PER_HOUR = 100;
|
|
|
|
const openai = AI_TEXT_GENERATION_ENABLED
|
|
? createOpenAI({ apiKey: process.env.OPENAI_SECRET_KEY })
|
|
: undefined;
|
|
|
|
// Allows 100 requests per hour
|
|
const ratelimit = HAS_VERCEL_KV
|
|
? new Ratelimit({
|
|
redis: kv,
|
|
limiter: Ratelimit.slidingWindow(RATE_LIMIT_MAX_QUERIES_PER_HOUR, '1h'),
|
|
})
|
|
: undefined;
|
|
|
|
export const streamOpenAiImageQuery = async (
|
|
imageBase64: string,
|
|
query: string,
|
|
) => {
|
|
return safelyRunAdminServerAction(async () => {
|
|
if (ratelimit) {
|
|
let success = false;
|
|
try {
|
|
success = (await ratelimit.limit(RATE_LIMIT_IDENTIFIER)).success;
|
|
} catch (e: any) {
|
|
console.error('Failed to rate limit OpenAI', e);
|
|
throw new Error('Failed to rate limit OpenAI');
|
|
}
|
|
if (!success) {
|
|
console.error('OpenAI rate limit exceeded');
|
|
throw new Error('OpenAI rate limit exceeded');
|
|
}
|
|
}
|
|
|
|
const stream = createStreamableValue('');
|
|
|
|
if (openai) {
|
|
(async () => {
|
|
const { textStream } = await streamText({
|
|
model: openai('gpt-4-vision-preview'),
|
|
messages: [{
|
|
'role': 'user',
|
|
'content': [
|
|
{
|
|
'type': 'text',
|
|
'text': query,
|
|
}, {
|
|
'type': 'image',
|
|
'image': removeBase64Prefix(imageBase64),
|
|
},
|
|
],
|
|
}],
|
|
});
|
|
for await (const delta of textStream) {
|
|
stream.update(delta);
|
|
}
|
|
stream.done();
|
|
})();
|
|
}
|
|
|
|
return stream.value;
|
|
});
|
|
};
|