Skip to content
Open
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
1,556 changes: 1,556 additions & 0 deletions ENTERPRISE_FEATURE_ANALYSIS.md

Large diffs are not rendered by default.

831 changes: 831 additions & 0 deletions docs/ENTERPRISE_ROADMAP.md

Large diffs are not rendered by default.

977 changes: 977 additions & 0 deletions docs/UPDATED_RECOMMENDATIONS.md

Large diffs are not rendered by default.

1,514 changes: 1,514 additions & 0 deletions docs/implementation-plans/01-api-layer.md

Large diffs are not rendered by default.

1,577 changes: 1,577 additions & 0 deletions docs/implementation-plans/02-data-platform-integrations.md

Large diffs are not rendered by default.

914 changes: 914 additions & 0 deletions docs/implementation-plans/03-elasticsearch-integration.md

Large diffs are not rendered by default.

933 changes: 933 additions & 0 deletions docs/implementation-plans/04-compliance-security-suite.md

Large diffs are not rendered by default.

1,022 changes: 1,022 additions & 0 deletions docs/implementation-plans/05-multi-llm-provider-support.md

Large diffs are not rendered by default.

1,703 changes: 1,554 additions & 149 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
"generate-embeddings": "tsx scripts/generate_embeddings.ts"
},
"dependencies": {
"@ai-sdk/anthropic": "^2.0.45",
"@ai-sdk/google": "^2.0.38",
"@ai-sdk/openai": "^2.0.68",
"@aws-sdk/client-textract": "^3.934.0",
"@radix-ui/react-checkbox": "^1.1.1",
Comment on lines +17 to 21
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

New AI/SDK dependencies align with the LLM/OCR features; verify version compatibility

The added @ai-sdk/*, ai, and @aws-sdk/client-textract dependencies 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:

Check the supported Node.js / React / Next.js versions and peer dependency requirements for:
- `ai@5.0.95`
- `@ai-sdk/openai@2.0.68`
- `@ai-sdk/anthropic@2.0.45`
- `@ai-sdk/google@2.0.38`
- `@aws-sdk/client-textract@3.934.0`
and confirm they are compatible with a Next.js 14.2.8 + React 18 app.

💡 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:

  • Next.js v14 requires Node >= 18.17 and is intended to be used with React 18. (Next.js upgrade notes). [1]
  • The AI SDK v5 (ai@5.x and the @ai-sdk/* v2 series) requires Node 18+ in its docs and additionally lists zod (>= 3.25.0) as a peer dependency for the v5 migration / providers. The migration/getting-started docs for AI SDK v5 show the ai package and @ai-sdk/* v2 series and call out zod as a required peer dependency. (@AI-SDK provider packages are the v2 series used by ai@5.x). [2][3]
  • Individual @AI-SDK provider packages (openai / anthropic / google) are part of the AI SDK v2 provider releases and are published as companion provider packages for ai@5 — their docs and package pages align with the AI SDK v5 requirements above. (Examples: Anthropic provider listing; OpenAI/Google providers are the same provider-series). [4]
  • @aws-sdk/client-textract (AWS SDK for JavaScript v3) targets modern Node.js LTS and AWS has announced v3 no longer supports Node 16.x; v3 is intended for Node 18+ (AWS SDK v3 Node support guidance). The modular v3 clients (including client-textract@3.934.0) do not impose React/Next peer deps and are compatible in server/runtime code (Node) used by Next.js. [5][6]

What to check in your project

  • Use Node >= 18.17 (Next.js 14 minimum). If your environment uses a lower Node, upgrade. [1]
  • Add/ensure zod >= 3.25.0 in your package.json if you use ai@5.x / @ai-sdk/* v2 features that declare it as a peer dependency. [2][3]
  • @aws-sdk/client-textract has no React/Next peer deps; install it normally for server-side usage (Node runtime). [5]

Sources

  • Next.js v14 upgrade / requirements (Node >=18.17, React 18). [1]
  • AI SDK v5 migration / getting-started (ai@5.x, @ai-sdk/* v2, and zod peer dep note). [2][3]
  • @ai-sdk/anthropic package listing / provider info (example provider page). [4]
  • AWS SDK for JavaScript (v3) Node support / end-of-support notices (Node 16 EOL; v3 targets Node 18+). [5][6]

If you want, I can fetch the exact peerDependencies sections from each package.json for these exact versions and paste them here.



---

🏁 Script executed:

```shell
#!/bin/bash
# Check for zod dependency in package.json
rg '"zod"' package.json -A 1

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
In package.json around lines 17 to 21, the project currently pins zod to ^3.23.8
which is below the ai@5.x peer requirement; update the zod entry to ^3.25.0 (or
a later 3.x release) in package.json, then run the package manager (npm install
or yarn install) to update the lockfile and ensure dependencies resolve; finally
run your test/build steps (or a simple start) to verify there are no runtime
peer-dependency issues.

"@radix-ui/react-collapsible": "^1.1.0",
"@radix-ui/react-dialog": "^1.1.4",
Expand All @@ -33,6 +37,7 @@
"@supabase/supabase-js": "^2.45.3",
"@types/pdfjs-dist": "^2.10.378",
"@types/uuid": "^10.0.0",
"ai": "^5.0.95",
"bcryptjs": "^2.4.3",
"checkbox": "^0.0.1",
"class-variance-authority": "^0.7.0",
Expand Down
138 changes: 138 additions & 0 deletions src/app/api/v1/chat/route.ts
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
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Clarify streaming format vs text/event-stream header

generateStreamingCompletion returns a plain text ReadableStream, but the response is advertised as text/event-stream. Unless textStream is already SSE‑framed, EventSource/SSE clients may misinterpret the stream. Consider either:

  • Using the Vercel AI helper that returns a proper SSE Response (e.g. toDataStreamResponse() upstream in generateStreamingCompletion), or
  • Changing the content type to a generic streaming type (e.g. text/plain) if consumers will read raw chunks via fetch/ReadableStream.

🌐 Web query:

What is the recommended way to return a streamed chat completion with Vercel AI SDK `streamText` so that clients receive a proper `text/event-stream` SSE response?

💡 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):

const result = streamText({ model: openai('gpt-4o-mini'), messages });
return result.toUIMessageStreamResponse({
  headers: {
    // optional extra headers
    'X-Chat-Id': chatId,
  },
});

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]




[/run_scripts]

---

🏁 Script executed:

```shell
#!/bin/bash
# Find and inspect generateStreamingCompletion implementation
rg -n "generateStreamingCompletion" --type=ts -B2 -A10

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 -A2

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Find where Response is constructed with text/event-stream
rg -n "text/event-stream" --type=ts -B5 -A5

Length of output: 736


[/run_scripts]

---

🏁 Script executed:

```shell
#!/bin/bash
# View the full generateStreamingCompletion implementation
sed -n '83,110p' src/lib/llm/llm-service.ts

Length of output: 729


Fix streaming response to use Vercel AI SDK's proper SSE helper

The review comment is correct. generateStreamingCompletion() currently returns result.textStream, which is raw text without SSE framing. While the route handler sets the text/event-stream header, the actual stream content is not SSE-formatted—clients expecting proper Server-Sent Events protocol will fail.

Per Vercel AI SDK recommendations, use result.toUIMessageStreamResponse() which automatically sets proper text/event-stream headers and protocol framing.

Required changes:

  1. src/lib/llm/llm-service.ts (line 83–108): Change generateStreamingCompletion() to return Promise<Response> and use result.toUIMessageStreamResponse() instead of result.textStream.
  2. src/app/api/v1/chat/route.ts (line 93–107): Update to directly return the Response from generateStreamingCompletion() instead of manually wrapping it.
🤖 Prompt for AI Agents
In src/app/api/v1/chat/route.ts around lines 91–108 and
src/lib/llm/llm-service.ts around lines 83–108, the streaming flow currently
returns a raw text stream and wraps it with SSE headers, but the stream lacks
SSE framing; update generateStreamingCompletion in llm-service.ts to return a
Promise<Response> using result.toUIMessageStreamResponse() (so the SDK applies
proper SSE framing and headers), and in route.ts simply await and return that
Response directly instead of re-wrapping the stream and headers.

} 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);
}
}
124 changes: 124 additions & 0 deletions src/app/api/v1/extract/route.ts
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
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Tighten input-source and base64 handling; avoid unnecessary cast

A few small issues in the input handling block:

  • The comment says “Either file (base64) or url must be provided”, but the logic only checks that at least one is present. If both are sent, file silently wins and url is ignored. Consider enforcing “exactly one” source, e.g. with a refinement on the Zod schema or an explicit if (file && url) validation error.
  • Buffer.from(file, 'base64') seldom throws for malformed base64; your try/catch will almost never return “Invalid base64 file data”, and bad input is more likely to fail later inside extractText as a 500. If you care about returning a clear 4xx, consider validating the base64 string (length/charset) or using a small helper to detect obviously invalid data before calling OCR.
  • method: method as OCRMethod forces an optional field into the non‑optional OCRMethod type. Since extractText already chooses a method when options?.method is falsy, you can drop the cast and allow method to be undefined naturally for better type safety.
  • Minor: if pages are intended as page numbers, z.array(z.number().positive()) will accept non‑integers; z.array(z.number().int().positive()) would better model that constraint.

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);
}
}
55 changes: 55 additions & 0 deletions src/app/api/v1/health/route.ts
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);
}
Loading