-
Notifications
You must be signed in to change notification settings - Fork 0
Research app features and market demands #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
e6286d0
551c12c
901020c
aa96908
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,138 @@ | ||
| /** | ||
| * Chat Completion Endpoint | ||
| * | ||
| * POST /api/v1/chat | ||
| * | ||
| * Generate chat completions using multi-provider LLM. | ||
| */ | ||
|
|
||
| import { NextRequest } from 'next/server'; | ||
| import { z } from 'zod'; | ||
| import { generateCompletion, generateStreamingCompletion, type LLMProviderType } from '@/lib/llm'; | ||
| import { | ||
| validateAPIKey, | ||
| hasPermission, | ||
| checkRateLimit, | ||
| authError, | ||
| rateLimitError, | ||
| } from '../utils/auth'; | ||
| import { ErrorResponses, handleError } from '../utils/errors'; | ||
|
|
||
| // Request validation schema | ||
| const ChatRequestSchema = z.object({ | ||
| messages: z.array( | ||
| z.object({ | ||
| role: z.enum(['system', 'user', 'assistant']), | ||
| content: z.string(), | ||
| }) | ||
| ), | ||
| provider: z.enum(['openai', 'anthropic', 'google', 'azure', 'ollama']).optional(), | ||
| model: z.string().optional(), | ||
| temperature: z.number().min(0).max(2).optional(), | ||
| maxTokens: z.number().positive().optional(), | ||
| stream: z.boolean().optional(), | ||
| }); | ||
|
|
||
| export async function POST(request: NextRequest) { | ||
| try { | ||
| // Authenticate | ||
| const keyInfo = await validateAPIKey(request); | ||
| if (!keyInfo) { | ||
| return authError('Invalid or missing API key'); | ||
| } | ||
|
|
||
| // Check permission | ||
| if (!hasPermission(keyInfo, 'chat')) { | ||
| return authError('API key does not have chat permission', 403); | ||
| } | ||
|
|
||
| // Check rate limit | ||
| const rateLimit = checkRateLimit(keyInfo); | ||
| if (!rateLimit.allowed) { | ||
| return rateLimitError(rateLimit.resetAt); | ||
| } | ||
|
|
||
| // Parse and validate request body | ||
| const body = await request.json(); | ||
| const validation = ChatRequestSchema.safeParse(body); | ||
|
|
||
| if (!validation.success) { | ||
| return ErrorResponses.validationError('Invalid request body', { | ||
| errors: validation.error.flatten().fieldErrors, | ||
| }); | ||
| } | ||
|
|
||
| const { messages, provider, model, temperature, maxTokens, stream } = validation.data; | ||
|
|
||
| // Determine provider and model | ||
| const llmProvider = (provider || 'openai') as LLMProviderType; | ||
| const llmModel = model || (llmProvider === 'openai' ? 'gpt-4o' : undefined); | ||
|
|
||
| if (!llmModel) { | ||
| return ErrorResponses.validationError('Model must be specified for this provider'); | ||
| } | ||
|
|
||
| // Build prompt from messages | ||
| const systemMessage = messages.find((m) => m.role === 'system')?.content; | ||
| const conversationMessages = messages.filter((m) => m.role !== 'system'); | ||
|
|
||
| // Format conversation for prompt | ||
| const prompt = conversationMessages | ||
| .map((m) => `${m.role === 'user' ? 'User' : 'Assistant'}: ${m.content}`) | ||
| .join('\n\n'); | ||
|
|
||
| const config = { | ||
| provider: llmProvider, | ||
| model: llmModel, | ||
| temperature, | ||
| maxTokens, | ||
| }; | ||
|
|
||
| if (stream) { | ||
| // Streaming response | ||
| const textStream = await generateStreamingCompletion({ | ||
| prompt, | ||
| config, | ||
| systemPrompt: systemMessage, | ||
| }); | ||
|
|
||
| // Return the stream with appropriate headers | ||
| return new Response(textStream, { | ||
| headers: { | ||
| 'Content-Type': 'text/event-stream', | ||
| 'Cache-Control': 'no-cache', | ||
| Connection: 'keep-alive', | ||
| 'X-RateLimit-Remaining': String(rateLimit.remaining), | ||
| 'X-RateLimit-Reset': String(rateLimit.resetAt), | ||
| }, | ||
| }); | ||
|
Comment on lines
+91
to
+108
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chainClarify streaming format vs
🌐 Web query: 💡 Result: Use streamText and return its built-in SSE response helper — call result.toUIMessageStreamResponse() (or toDataStreamResponse for the simpler "data" protocol) and return that Response so the SDK sets the proper text/event-stream headers and protocol header for you. Example (Node / Vercel function): This yields a Response with Content-Type: text/event-stream; charset=utf-8 and x-vercel-ai-ui-message-stream: v1 (uses the SDK's UI message stream conversion). [1][2][3] Sources: Official SDK internals and examples. [1] [2] [3] Length of output: 2932 🏁 Script executed: #!/bin/bash
# Search for imports of Vercel AI SDK to understand what's being used
rg -n "streamText\|toUIMessageStreamResponse\|toDataStreamResponse" --type=ts -B2 -A2Length of output: 0 🏁 Script executed: #!/bin/bash
# Find where Response is constructed with text/event-stream
rg -n "text/event-stream" --type=ts -B5 -A5Length of output: 736 Length of output: 729 Fix streaming response to use Vercel AI SDK's proper SSE helper The review comment is correct. Per Vercel AI SDK recommendations, use Required changes:
🤖 Prompt for AI Agents |
||
| } else { | ||
| // Non-streaming response | ||
| const result = await generateCompletion({ | ||
| prompt, | ||
| config, | ||
| systemPrompt: systemMessage, | ||
| }); | ||
|
|
||
| return Response.json( | ||
| { | ||
| success: true, | ||
| data: { | ||
| content: result.text, | ||
| usage: result.usage, | ||
| model: llmModel, | ||
| provider: llmProvider, | ||
| }, | ||
| }, | ||
| { | ||
| headers: { | ||
| 'X-RateLimit-Remaining': String(rateLimit.remaining), | ||
| 'X-RateLimit-Reset': String(rateLimit.resetAt), | ||
| }, | ||
| } | ||
| ); | ||
| } | ||
| } catch (error) { | ||
| return handleError(error); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,124 @@ | ||
| /** | ||
| * Document Extraction Endpoint | ||
| * | ||
| * POST /api/v1/extract | ||
| * | ||
| * Extract text from PDF documents using OCR. | ||
| */ | ||
|
|
||
| import { NextRequest } from 'next/server'; | ||
| import { z } from 'zod'; | ||
| import { extractText, type OCRMethod } from '@/lib/ocr'; | ||
| import { | ||
| validateAPIKey, | ||
| hasPermission, | ||
| checkRateLimit, | ||
| authError, | ||
| rateLimitError, | ||
| } from '../utils/auth'; | ||
| import { ErrorResponses, handleError } from '../utils/errors'; | ||
|
|
||
| // Request validation schema | ||
| const ExtractRequestSchema = z.object({ | ||
| // Either file (base64) or url must be provided | ||
| file: z.string().optional(), | ||
| url: z.string().url().optional(), | ||
| // OCR options | ||
| method: z.enum(['llmwhisperer', 'deepseek', 'textract', 'tesseract', 'pdfjs']).optional(), | ||
| preserveLayout: z.boolean().optional(), | ||
| detectForms: z.boolean().optional(), | ||
| extractTables: z.boolean().optional(), | ||
| pages: z.array(z.number().positive()).optional(), | ||
| language: z.string().optional(), | ||
| password: z.string().optional(), | ||
| }); | ||
|
|
||
| export async function POST(request: NextRequest) { | ||
| try { | ||
| // Authenticate | ||
| const keyInfo = await validateAPIKey(request); | ||
| if (!keyInfo) { | ||
| return authError('Invalid or missing API key'); | ||
| } | ||
|
|
||
| // Check permission | ||
| if (!hasPermission(keyInfo, 'extract')) { | ||
| return authError('API key does not have extraction permission', 403); | ||
| } | ||
|
|
||
| // Check rate limit | ||
| const rateLimit = checkRateLimit(keyInfo); | ||
| if (!rateLimit.allowed) { | ||
| return rateLimitError(rateLimit.resetAt); | ||
| } | ||
|
|
||
| // Parse and validate request body | ||
| const body = await request.json(); | ||
| const validation = ExtractRequestSchema.safeParse(body); | ||
|
|
||
| if (!validation.success) { | ||
| return ErrorResponses.validationError('Invalid request body', { | ||
| errors: validation.error.flatten().fieldErrors, | ||
| }); | ||
| } | ||
|
|
||
| const { file, url, method, preserveLayout, detectForms, extractTables, pages, language, password } = | ||
| validation.data; | ||
|
|
||
| // Ensure either file or url is provided | ||
| if (!file && !url) { | ||
| return ErrorResponses.validationError('Either file (base64) or url must be provided'); | ||
| } | ||
|
|
||
| // Convert base64 to buffer if file provided | ||
| let input: Buffer | string; | ||
| if (file) { | ||
| try { | ||
| input = Buffer.from(file, 'base64'); | ||
| } catch { | ||
| return ErrorResponses.validationError('Invalid base64 file data'); | ||
| } | ||
| } else { | ||
| input = url!; | ||
| } | ||
|
Comment on lines
+65
to
+83
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Tighten input-source and base64 handling; avoid unnecessary cast A few small issues in the input handling block:
These refinements would make the endpoint behavior more predictable and validation errors clearer without changing the overall design. Also applies to: 85-97 |
||
|
|
||
| // Extract text | ||
| const result = await extractText({ | ||
| input, | ||
| options: { | ||
| method: method as OCRMethod, | ||
| preserveLayout, | ||
| detectForms, | ||
| extractTables, | ||
| pages, | ||
| language, | ||
| password, | ||
| }, | ||
| }); | ||
|
|
||
| // Return result with rate limit headers | ||
| return Response.json( | ||
| { | ||
| success: true, | ||
| data: { | ||
| text: result.text, | ||
| markdown: result.markdown, | ||
| confidence: result.confidence, | ||
| method: result.method, | ||
| pageCount: result.pageCount, | ||
| processingTimeMs: result.processingTimeMs, | ||
| tables: result.tables, | ||
| formFields: result.formFields, | ||
| }, | ||
| }, | ||
| { | ||
| headers: { | ||
| 'X-RateLimit-Remaining': String(rateLimit.remaining), | ||
| 'X-RateLimit-Reset': String(rateLimit.resetAt), | ||
| }, | ||
| } | ||
| ); | ||
| } catch (error) { | ||
| return handleError(error); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| /** | ||
| * Health Check Endpoint | ||
| * | ||
| * GET /api/v1/health | ||
| * | ||
| * Returns service health status and available features. | ||
| */ | ||
|
|
||
| import { NextRequest } from 'next/server'; | ||
| import { isProviderAvailable, getAvailableProviders } from '@/lib/llm'; | ||
| import { getLLMWhispererQuota } from '@/lib/ocr'; | ||
|
|
||
| export async function GET(request: NextRequest) { | ||
| const startTime = Date.now(); | ||
|
|
||
| // Check service health | ||
| const [llmProviders, ocrQuota] = await Promise.all([ | ||
| getAvailableProviders(), | ||
| getLLMWhispererQuota(), | ||
| ]); | ||
|
|
||
| const health = { | ||
| status: 'healthy', | ||
| timestamp: new Date().toISOString(), | ||
| version: '1.0.0', | ||
| responseTimeMs: Date.now() - startTime, | ||
| services: { | ||
| llm: { | ||
| status: llmProviders.length > 0 ? 'available' : 'degraded', | ||
| providers: llmProviders, | ||
| }, | ||
| ocr: { | ||
| status: ocrQuota ? 'available' : 'limited', | ||
| llmwhisperer: ocrQuota | ||
| ? { | ||
| remaining: ocrQuota.remaining, | ||
| limit: ocrQuota.limit, | ||
| } | ||
| : null, | ||
| textract: !!process.env.AWS_ACCESS_KEY_ID, | ||
| deepseek: !!process.env.DEEPSEEK_OCR_ENDPOINT, | ||
| }, | ||
| database: { | ||
| status: process.env.NEXT_PUBLIC_SUPABASE_URL ? 'configured' : 'not_configured', | ||
| }, | ||
| }, | ||
| }; | ||
|
|
||
| // Determine overall status | ||
| if (llmProviders.length === 0 && !ocrQuota) { | ||
| health.status = 'degraded'; | ||
| } | ||
|
|
||
| return Response.json(health); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
New AI/SDK dependencies align with the LLM/OCR features; verify version compatibility
The added
@ai-sdk/*,ai, and@aws-sdk/client-textractdependencies match the new multi‑provider LLM and Textract usage. From a structure standpoint this looks fine; just ensure the chosen versions are compatible with your Next 14 / React 18 / Node.js runtime and each other (peer deps, bundling, ESM/CJS).Also applies to: 40-40
🌐 Web query:
💡 Result:
Short answer: Yes — these package versions are compatible with a Next.js 14.2.8 + React 18 app, provided your runtime Node.js meets the Next.js v14 minimum (Node 18.17+) and you satisfy the AI SDK peer deps (notably zod >= 3.25.0). Evidence:
What to check in your project
Sources
If you want, I can fetch the exact peerDependencies sections from each package.json for these exact versions and paste them here.
Length of output: 25
Upgrade zod to satisfy ai@5.x peer dependency requirement
The added dependencies are compatible with Next.js 14.2.8 + React 18 (Node 18.17+), but zod needs to be upgraded. The AI SDK v5 package (ai@5.0.95) requires zod >= 3.25.0 as a peer dependency. Currently, package.json specifies zod@^3.23.8, which falls below the minimum. Update to zod@^3.25.0 or higher to prevent runtime failures when using the AI SDK providers.
🤖 Prompt for AI Agents