claudmax 2.0.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/claudmax-1.0.16.tgz +0 -0
- package/{packages/cli/index.js → index.js} +2 -0
- package/package.json +27 -55
- package/.claude/settings.local.json +0 -7
- package/.env.example +0 -24
- package/.github/workflows/publish.yml +0 -31
- package/README.md +0 -178
- package/claudmax-mcp-1.0.2.tgz +0 -0
- package/help +0 -0
- package/help-wal +0 -0
- package/next-env.d.ts +0 -6
- package/next.config.mjs +0 -43
- package/packages/cli/claudmax-1.0.16.tgz +0 -0
- package/packages/cli/package.json +0 -33
- package/packages/mcp/claudmax-mcp-1.0.0.tgz +0 -0
- package/packages/mcp/claudmax-mcp-1.0.1.tgz +0 -0
- package/packages/mcp/claudmax-mcp-1.0.2.tgz +0 -0
- package/packages/mcp/claudmax-mcp-1.0.3.tgz +0 -0
- package/packages/mcp/index.js +0 -129
- package/packages/mcp/package-lock.json +0 -1146
- package/packages/mcp/package.json +0 -32
- package/postcss.config.mjs +0 -6
- package/prisma/schema.prisma +0 -130
- package/prisma/seed.ts +0 -27
- package/public/favicon.svg +0 -10
- package/public/robots.txt +0 -10
- package/run_build.sh +0 -4
- package/scripts/migrate-plans.js +0 -98
- package/scripts/seed-blog.ts +0 -1014
- package/src/app/admin/dashboard/AdminDashboardClient.tsx +0 -1546
- package/src/app/admin/dashboard/page.tsx +0 -13
- package/src/app/admin/page.tsx +0 -132
- package/src/app/api/admin/auth/me/route.ts +0 -34
- package/src/app/api/admin/health/route.ts +0 -110
- package/src/app/api/admin/keys/[id]/route.ts +0 -116
- package/src/app/api/admin/keys/route.ts +0 -192
- package/src/app/api/admin/keys-list/route.ts +0 -81
- package/src/app/api/admin/login/route.ts +0 -72
- package/src/app/api/admin/logout/route.ts +0 -8
- package/src/app/api/admin/migrate/route.ts +0 -133
- package/src/app/api/admin/plans/[id]/route.ts +0 -65
- package/src/app/api/admin/plans/route.ts +0 -66
- package/src/app/api/admin/posts/[id]/route.ts +0 -81
- package/src/app/api/admin/posts/route.ts +0 -83
- package/src/app/api/admin/seed/route.ts +0 -145
- package/src/app/api/admin/settings/route.ts +0 -44
- package/src/app/api/admin/stats/route.ts +0 -74
- package/src/app/api/admin/users/[id]/route.ts +0 -166
- package/src/app/api/admin/users/plans/route.ts +0 -45
- package/src/app/api/admin/users/route.ts +0 -202
- package/src/app/api/blog/[slug]/route.ts +0 -22
- package/src/app/api/blog/route.ts +0 -40
- package/src/app/api/cron/daily-status/route.ts +0 -208
- package/src/app/api/support/chat/route.ts +0 -55
- package/src/app/api/support/chat/session/route.ts +0 -62
- package/src/app/api/support/chat/stream/route.ts +0 -44
- package/src/app/api/support/email/route.ts +0 -63
- package/src/app/api/tools/understand_image/route.ts +0 -113
- package/src/app/api/tools/upload/route.ts +0 -179
- package/src/app/api/tools/web_search/route.ts +0 -99
- package/src/app/api/v1/audio/route.ts +0 -67
- package/src/app/api/v1/audio/speech/route.ts +0 -73
- package/src/app/api/v1/chat/completions/route.ts +0 -3
- package/src/app/api/v1/chat/route.ts +0 -1079
- package/src/app/api/v1/images/generations/route.ts +0 -93
- package/src/app/api/v1/info/route.ts +0 -30
- package/src/app/api/v1/key-status/route.ts +0 -109
- package/src/app/api/v1/key-status/stream/route.ts +0 -135
- package/src/app/api/v1/messages/count_tokens/route.ts +0 -22
- package/src/app/api/v1/messages/route.ts +0 -807
- package/src/app/api/v1/models/route.ts +0 -14
- package/src/app/api/v1/route.ts +0 -18
- package/src/app/blog/BlogClient.tsx +0 -193
- package/src/app/blog/[slug]/page.tsx +0 -117
- package/src/app/blog/page.tsx +0 -20
- package/src/app/check-usage/CheckUsageClient.tsx +0 -186
- package/src/app/check-usage/layout.tsx +0 -11
- package/src/app/check-usage/page.tsx +0 -15
- package/src/app/docs/layout.tsx +0 -16
- package/src/app/docs/page.tsx +0 -1055
- package/src/app/faq/FAQClient.tsx +0 -227
- package/src/app/faq/page.tsx +0 -21
- package/src/app/globals.css +0 -75
- package/src/app/layout.tsx +0 -80
- package/src/app/page.tsx +0 -256
- package/src/app/reseller/ResellerClient.tsx +0 -435
- package/src/app/reseller/page.tsx +0 -15
- package/src/app/setup.ps1/route.ts +0 -79
- package/src/app/setup.sh/route.ts +0 -113
- package/src/app/sitemap.ts +0 -50
- package/src/app/status/StatusClient.tsx +0 -103
- package/src/app/status/layout.tsx +0 -11
- package/src/app/status/page.tsx +0 -15
- package/src/app/support/SupportClient.tsx +0 -411
- package/src/app/support/page.tsx +0 -25
- package/src/app/v1/chat/completions/route.ts +0 -3
- package/src/app/v1/chat/route.ts +0 -4
- package/src/app/v1/messages/route.ts +0 -3
- package/src/components/Footer.tsx +0 -120
- package/src/components/Header.tsx +0 -131
- package/src/components/landing/features.tsx +0 -99
- package/src/components/ui/badge.tsx +0 -32
- package/src/components/ui/button.tsx +0 -46
- package/src/components/ui/card.tsx +0 -50
- package/src/components/ui/dialog.tsx +0 -97
- package/src/components/ui/dropdown-menu.tsx +0 -156
- package/src/components/ui/input.tsx +0 -21
- package/src/components/ui/label.tsx +0 -15
- package/src/components/ui/separator.tsx +0 -22
- package/src/components/ui/switch.tsx +0 -27
- package/src/components/ui/tabs.tsx +0 -51
- package/src/components/ui/toast.tsx +0 -103
- package/src/lib/auth.ts +0 -45
- package/src/lib/prisma.ts +0 -20
- package/src/lib/providers.ts +0 -158
- package/src/lib/security.ts +0 -165
- package/src/lib/utils.ts +0 -14
- package/src/middleware.ts +0 -30
- package/tailwind.config.ts +0 -53
- package/tsconfig.json +0 -41
- package/tsconfig.tsbuildinfo +0 -1
- package/vercel.json +0 -8
- /package/{packages/cli/bin → bin}/claudmax.js +0 -0
- /package/{packages/cli/claudmax-1.0.17.tgz → claudmax-1.0.17.tgz} +0 -0
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
import { NextResponse } from 'next/server';
|
|
2
|
-
import { prisma } from '@/lib/prisma';
|
|
3
|
-
|
|
4
|
-
const OPENROUTER_API_URL = 'https://openrouter.ai/api/v1/chat/completions';
|
|
5
|
-
|
|
6
|
-
async function validateKey(apiKey: string) {
|
|
7
|
-
if (!apiKey?.startsWith('sk-cmx_')) return { error: 'Invalid API key', status: 401 };
|
|
8
|
-
const key = await prisma.apiKey.findUnique({ where: { key: apiKey } });
|
|
9
|
-
if (!key) return { error: 'Invalid API key', status: 401 };
|
|
10
|
-
if (!key.isActive) return { error: 'API key revoked', status: 401 };
|
|
11
|
-
return { key };
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function toBase64(arrayBuffer: ArrayBuffer): string {
|
|
15
|
-
const bytes = new Uint8Array(arrayBuffer);
|
|
16
|
-
let binary = '';
|
|
17
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
18
|
-
binary += String.fromCharCode(bytes[i]);
|
|
19
|
-
}
|
|
20
|
-
return btoa(binary);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function getMimeType(file: File | Blob): string {
|
|
24
|
-
if ('type' in file && file.type) return file.type;
|
|
25
|
-
const name = ('name' in file ? file.name : 'file') as string;
|
|
26
|
-
const ext = name.split('.').pop()?.toLowerCase() ?? '';
|
|
27
|
-
const map: Record<string, string> = {
|
|
28
|
-
pdf: 'application/pdf',
|
|
29
|
-
txt: 'text/plain',
|
|
30
|
-
md: 'text/markdown',
|
|
31
|
-
json: 'application/json',
|
|
32
|
-
html: 'text/html',
|
|
33
|
-
css: 'text/css',
|
|
34
|
-
js: 'application/javascript',
|
|
35
|
-
csv: 'text/csv',
|
|
36
|
-
png: 'image/png',
|
|
37
|
-
jpg: 'image/jpeg',
|
|
38
|
-
jpeg: 'image/jpeg',
|
|
39
|
-
gif: 'image/gif',
|
|
40
|
-
webp: 'image/webp',
|
|
41
|
-
svg: 'image/svg+xml',
|
|
42
|
-
mp3: 'audio/mpeg',
|
|
43
|
-
mp4: 'video/mp4',
|
|
44
|
-
};
|
|
45
|
-
return map[ext] ?? 'application/octet-stream';
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function isImage(mimeType: string): boolean {
|
|
49
|
-
return ['image/png', 'image/jpeg', 'image/jpg', 'image/gif', 'image/webp', 'image/svg+xml'].includes(mimeType);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function isText(mimeType: string): boolean {
|
|
53
|
-
return mimeType.startsWith('text/') || ['application/json', 'application/javascript'].includes(mimeType);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export async function POST(req: Request) {
|
|
57
|
-
try {
|
|
58
|
-
const apiKey = req.headers.get('x-api-key') ?? req.headers.get('authorization')?.replace('Bearer ', '');
|
|
59
|
-
if (!apiKey) {
|
|
60
|
-
return NextResponse.json({ error: 'Missing API key' }, { status: 401 });
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const val = await validateKey(apiKey);
|
|
64
|
-
if ('error' in val) {
|
|
65
|
-
return NextResponse.json({ error: val.error }, { status: val.status });
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Support both JSON and multipart
|
|
69
|
-
const contentType = req.headers.get('content-type') ?? '';
|
|
70
|
-
let files: Array<{ name: string; mimeType: string; base64: string; size: number }> = [];
|
|
71
|
-
let prompt = 'Describe or analyze this file.';
|
|
72
|
-
|
|
73
|
-
if (contentType.includes('multipart/form-data')) {
|
|
74
|
-
const formData = await req.formData();
|
|
75
|
-
prompt = (formData.get('prompt') as string | null) ?? prompt;
|
|
76
|
-
|
|
77
|
-
for (const [key, value] of formData.entries()) {
|
|
78
|
-
if (key === 'prompt') continue;
|
|
79
|
-
if (value instanceof File) {
|
|
80
|
-
const buffer = await value.arrayBuffer();
|
|
81
|
-
const base64 = toBase64(buffer);
|
|
82
|
-
files.push({
|
|
83
|
-
name: value.name,
|
|
84
|
-
mimeType: getMimeType(value),
|
|
85
|
-
base64,
|
|
86
|
-
size: value.size,
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
} else {
|
|
91
|
-
// JSON mode: {"files": [{"name": "...", "mime_type": "...", "data": "base64..."}]}
|
|
92
|
-
const body = await req.json();
|
|
93
|
-
files = (body.files ?? []).map((f: Record<string, unknown>) => ({
|
|
94
|
-
name: (f.name as string) ?? 'file',
|
|
95
|
-
mimeType: (f.mime_type as string) ?? 'application/octet-stream',
|
|
96
|
-
base64: f.data as string,
|
|
97
|
-
size: (f.data as string).length,
|
|
98
|
-
}));
|
|
99
|
-
prompt = (body.prompt as string | null) ?? prompt;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (files.length === 0) {
|
|
103
|
-
return NextResponse.json({ error: 'At least one file is required' }, { status: 400 });
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const processed: Array<{
|
|
107
|
-
name: string;
|
|
108
|
-
type: string;
|
|
109
|
-
content?: string;
|
|
110
|
-
description?: string;
|
|
111
|
-
error?: string;
|
|
112
|
-
}> = [];
|
|
113
|
-
|
|
114
|
-
for (const file of files) {
|
|
115
|
-
if (isImage(file.mimeType)) {
|
|
116
|
-
// Send to vision model
|
|
117
|
-
const visionRes = await fetch(OPENROUTER_API_URL, {
|
|
118
|
-
method: 'POST',
|
|
119
|
-
headers: {
|
|
120
|
-
'Authorization': `Bearer ${process.env.OPENROUTER_API_KEY}`,
|
|
121
|
-
'Content-Type': 'application/json',
|
|
122
|
-
'HTTP-Referer': process.env.OPENROUTER_SITE_URL ?? 'https://claudmax.pro',
|
|
123
|
-
'X-Title': 'ClaudMax',
|
|
124
|
-
},
|
|
125
|
-
body: JSON.stringify({
|
|
126
|
-
model: 'qwen/qwen3.6-plus:free',
|
|
127
|
-
messages: [
|
|
128
|
-
{
|
|
129
|
-
role: 'user',
|
|
130
|
-
content: [
|
|
131
|
-
{ type: 'image_url', image_url: { url: `data:${file.mimeType};base64,${file.base64}` } },
|
|
132
|
-
{ type: 'text', text: prompt },
|
|
133
|
-
],
|
|
134
|
-
},
|
|
135
|
-
],
|
|
136
|
-
max_tokens: 2048,
|
|
137
|
-
temperature: 0.3,
|
|
138
|
-
}),
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
if (!visionRes.ok) {
|
|
142
|
-
const err = await visionRes.json().catch(() => ({}));
|
|
143
|
-
processed.push({ name: file.name, type: 'image', error: (err as any)?.error?.message ?? 'Image analysis failed' });
|
|
144
|
-
continue;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const data = await visionRes.json();
|
|
148
|
-
const description = data.choices?.[0]?.message?.content ?? 'Unable to analyze image.';
|
|
149
|
-
processed.push({ name: file.name, type: 'image', description, content: description });
|
|
150
|
-
} else if (isText(file.mimeType)) {
|
|
151
|
-
// Decode base64 to text
|
|
152
|
-
try {
|
|
153
|
-
// Handle base64 with or without data URL prefix
|
|
154
|
-
const base64Clean = file.base64.replace(/^data:[^;]+;base64,/, '');
|
|
155
|
-
const binary = atob(base64Clean);
|
|
156
|
-
const bytes = new Uint8Array(binary.length);
|
|
157
|
-
for (let i = 0; i < binary.length; i++) {
|
|
158
|
-
bytes[i] = binary.charCodeAt(i);
|
|
159
|
-
}
|
|
160
|
-
const text = new TextDecoder().decode(bytes);
|
|
161
|
-
processed.push({ name: file.name, type: 'text', content: text.slice(0, 10000) });
|
|
162
|
-
} catch {
|
|
163
|
-
processed.push({ name: file.name, type: 'text', error: 'Failed to decode text file' });
|
|
164
|
-
}
|
|
165
|
-
} else {
|
|
166
|
-
processed.push({ name: file.name, type: 'binary', error: `Unsupported file type: ${file.mimeType}` });
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
return NextResponse.json({
|
|
171
|
-
type: 'files_processed',
|
|
172
|
-
count: processed.length,
|
|
173
|
-
files: processed,
|
|
174
|
-
});
|
|
175
|
-
} catch (err) {
|
|
176
|
-
console.error('[ClaudMax] File upload error:', err);
|
|
177
|
-
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
|
178
|
-
}
|
|
179
|
-
}
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import { NextResponse } from 'next/server';
|
|
2
|
-
import { prisma } from '@/lib/prisma';
|
|
3
|
-
|
|
4
|
-
const OPENROUTER_API_URL = 'https://openrouter.ai/api/v1/chat/completions';
|
|
5
|
-
|
|
6
|
-
async function validateKey(apiKey: string) {
|
|
7
|
-
if (!apiKey?.startsWith('sk-cmx_')) return { error: 'Invalid API key', status: 401 };
|
|
8
|
-
const key = await prisma.apiKey.findUnique({ where: { key: apiKey } });
|
|
9
|
-
if (!key) return { error: 'Invalid API key', status: 401 };
|
|
10
|
-
if (!key.isActive) return { error: 'API key revoked', status: 401 };
|
|
11
|
-
return { key };
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export async function POST(req: Request) {
|
|
15
|
-
try {
|
|
16
|
-
const apiKey = req.headers.get('x-api-key') ?? req.headers.get('authorization')?.replace('Bearer ', '');
|
|
17
|
-
if (!apiKey) {
|
|
18
|
-
return NextResponse.json({ error: 'Missing API key' }, { status: 401 });
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const val = await validateKey(apiKey);
|
|
22
|
-
if ('error' in val) {
|
|
23
|
-
return NextResponse.json({ error: val.error }, { status: val.status });
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const { query } = await req.json();
|
|
27
|
-
if (!query) {
|
|
28
|
-
return NextResponse.json({ error: 'Query is required' }, { status: 400 });
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// Try Gemini Flash first (fast, great for search summarization), fall back to Qwen
|
|
32
|
-
const models = ['google/gemini-2.0-flash-exp:free', 'qwen/qwen2.5-72b-instruct:free'];
|
|
33
|
-
let searchRes: Response | null = null;
|
|
34
|
-
let lastError = '';
|
|
35
|
-
|
|
36
|
-
for (const model of models) {
|
|
37
|
-
searchRes = await fetch(OPENROUTER_API_URL, {
|
|
38
|
-
method: 'POST',
|
|
39
|
-
headers: {
|
|
40
|
-
'Authorization': `Bearer ${process.env.OPENROUTER_API_KEY}`,
|
|
41
|
-
'Content-Type': 'application/json',
|
|
42
|
-
'HTTP-Referer': process.env.OPENROUTER_SITE_URL ?? 'https://claudmax.pro',
|
|
43
|
-
'X-Title': 'ClaudMax',
|
|
44
|
-
},
|
|
45
|
-
body: JSON.stringify({
|
|
46
|
-
model,
|
|
47
|
-
messages: [
|
|
48
|
-
{
|
|
49
|
-
role: 'user',
|
|
50
|
-
content: `You are a web search assistant. Search the web for: "${query}". Provide 5 relevant search results with title, URL, and a brief snippet/summary of each result. Format your response as a JSON array with objects containing: title, url, snippet.`,
|
|
51
|
-
},
|
|
52
|
-
],
|
|
53
|
-
max_tokens: 2048,
|
|
54
|
-
temperature: 0.3,
|
|
55
|
-
}),
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
if (searchRes.ok) break;
|
|
59
|
-
const err = await searchRes.json().catch(() => ({}));
|
|
60
|
-
lastError = (err as any)?.error?.message ?? 'Search failed';
|
|
61
|
-
// If rate-limited, try next model; otherwise break
|
|
62
|
-
if (searchRes.status !== 429) break;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (!searchRes?.ok) {
|
|
66
|
-
return NextResponse.json(
|
|
67
|
-
{ error: { type: 'upstream_error', message: lastError || 'Search failed' } },
|
|
68
|
-
{ status: searchRes?.status ?? 500 }
|
|
69
|
-
);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const data = await searchRes.json();
|
|
73
|
-
const text = data.choices?.[0]?.message?.content ?? '';
|
|
74
|
-
|
|
75
|
-
// Try to parse as JSON, fall back to text
|
|
76
|
-
try {
|
|
77
|
-
// Strip markdown code blocks if present
|
|
78
|
-
const cleaned = text.replace(/^```json\s*/i, '').replace(/```$/i, '').trim();
|
|
79
|
-
const parsed = JSON.parse(cleaned);
|
|
80
|
-
return NextResponse.json({ results: parsed, query });
|
|
81
|
-
} catch {
|
|
82
|
-
// Return as formatted text results
|
|
83
|
-
return NextResponse.json({
|
|
84
|
-
results: [
|
|
85
|
-
{
|
|
86
|
-
title: `Web search results for: ${query}`,
|
|
87
|
-
url: `https://www.google.com/search?q=${encodeURIComponent(query)}`,
|
|
88
|
-
snippet: text.slice(0, 500),
|
|
89
|
-
},
|
|
90
|
-
],
|
|
91
|
-
query,
|
|
92
|
-
raw: text,
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
} catch (err) {
|
|
96
|
-
console.error('[ClaudMax] Web search error:', err);
|
|
97
|
-
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
|
98
|
-
}
|
|
99
|
-
}
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import { NextResponse } from 'next/server';
|
|
2
|
-
import { prisma } from '@/lib/prisma';
|
|
3
|
-
|
|
4
|
-
const TIER_LIMITS: Record<string, { requestsPerWindow: number; tokensPerWindow: number; windowHours: number }> = {
|
|
5
|
-
free: { requestsPerWindow: 100, tokensPerWindow: 500_000, windowHours: 5 },
|
|
6
|
-
'5x': { requestsPerWindow: 500, tokensPerWindow: 5_000_000, windowHours: 5 },
|
|
7
|
-
'20x': { requestsPerWindow: 2000, tokensPerWindow: 20_000_000, windowHours: 5 },
|
|
8
|
-
unlimited: { requestsPerWindow: 999_999_999, tokensPerWindow: 999_999_999_999, windowHours: 5 },
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
const usageStore = new Map<string, { requests: number; tokens: number; windowStart: number }>();
|
|
12
|
-
|
|
13
|
-
function getUsage(keyId: string) {
|
|
14
|
-
const now = Date.now();
|
|
15
|
-
let u = usageStore.get(keyId);
|
|
16
|
-
if (!u) {
|
|
17
|
-
u = { requests: 0, tokens: 0, windowStart: now };
|
|
18
|
-
usageStore.set(keyId, u);
|
|
19
|
-
}
|
|
20
|
-
if (now - u.windowStart > 5 * 60 * 60 * 1000) {
|
|
21
|
-
u.requests = 0;
|
|
22
|
-
u.tokens = 0;
|
|
23
|
-
u.windowStart = now;
|
|
24
|
-
}
|
|
25
|
-
return u;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
async function validateKey(apiKey: string) {
|
|
29
|
-
if (!apiKey?.startsWith('sk-cmx_')) return { error: 'Invalid API key format', status: 401 };
|
|
30
|
-
const key = await prisma.apiKey.findUnique({ where: { key: apiKey } });
|
|
31
|
-
if (!key) return { error: 'Invalid API key', status: 401 };
|
|
32
|
-
if (!key.isActive) return { error: 'API key revoked', status: 401 };
|
|
33
|
-
const tier = key.tier ?? 'free';
|
|
34
|
-
const limits = TIER_LIMITS[tier] ?? TIER_LIMITS.free;
|
|
35
|
-
const usage = getUsage(key.id);
|
|
36
|
-
if (usage.requests >= limits.requestsPerWindow) {
|
|
37
|
-
return { error: 'Rate limit exceeded', status: 429 };
|
|
38
|
-
}
|
|
39
|
-
if (usage.tokens >= limits.tokensPerWindow) {
|
|
40
|
-
return { error: 'Token limit exceeded', status: 429 };
|
|
41
|
-
}
|
|
42
|
-
return { key, usage, tier, limits };
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export async function POST(req: Request) {
|
|
46
|
-
try {
|
|
47
|
-
const apiKey = req.headers.get('x-api-key') ?? req.headers.get('authorization')?.replace('Bearer ', '');
|
|
48
|
-
if (!apiKey) {
|
|
49
|
-
return NextResponse.json({ error: 'Missing API key' }, { status: 401 });
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const val = await validateKey(apiKey);
|
|
53
|
-
if ('error' in val) {
|
|
54
|
-
return NextResponse.json({ error: val.error }, { status: val.status });
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return NextResponse.json({
|
|
58
|
-
error: {
|
|
59
|
-
type: 'not_implemented',
|
|
60
|
-
message: 'Audio generation is not yet available. OpenRouter does not expose a TTS generations endpoint. This feature requires a dedicated TTS provider integration.',
|
|
61
|
-
},
|
|
62
|
-
}, { status: 501 });
|
|
63
|
-
} catch (err) {
|
|
64
|
-
console.error('[ClaudMax] Audio error:', err);
|
|
65
|
-
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
|
66
|
-
}
|
|
67
|
-
}
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import { NextResponse } from 'next/server';
|
|
2
|
-
import { prisma } from '@/lib/prisma';
|
|
3
|
-
|
|
4
|
-
const OPENAI_TTS_URL = 'https://api.openai.com/v1/audio/speech';
|
|
5
|
-
|
|
6
|
-
export async function POST(req: Request) {
|
|
7
|
-
try {
|
|
8
|
-
const apiKey = req.headers.get('x-api-key') ?? req.headers.get('authorization')?.replace('Bearer ', '');
|
|
9
|
-
if (!apiKey) return NextResponse.json({ error: { message: 'Missing API key', type: 'authentication_error' } }, { status: 401 });
|
|
10
|
-
if (!apiKey.startsWith('sk-cmx_')) return NextResponse.json({ error: { message: 'Invalid API key format', type: 'authentication_error' } }, { status: 401 });
|
|
11
|
-
|
|
12
|
-
const key = await prisma.apiKey.findUnique({ where: { key: apiKey } });
|
|
13
|
-
if (!key) return NextResponse.json({ error: { message: 'Invalid API key', type: 'authentication_error' } }, { status: 401 });
|
|
14
|
-
if (!key.isActive) return NextResponse.json({ error: { message: 'API key has been revoked', type: 'authentication_error' } }, { status: 401 });
|
|
15
|
-
|
|
16
|
-
const body = await req.json();
|
|
17
|
-
const { model = 'tts-1', input, voice = 'alloy', response_format = 'mp3', speed = 1.0 } = body;
|
|
18
|
-
|
|
19
|
-
if (!input || typeof input !== 'string') {
|
|
20
|
-
return NextResponse.json({ error: { message: 'input is required', type: 'invalid_request_error' } }, { status: 400 });
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const openaiKey = process.env.OPENAI_API_KEY;
|
|
24
|
-
if (!openaiKey) {
|
|
25
|
-
return NextResponse.json({
|
|
26
|
-
error: {
|
|
27
|
-
message: 'Audio TTS requires OPENAI_API_KEY to be configured. Add it to your .env file: OPENAI_API_KEY=sk-...',
|
|
28
|
-
type: 'configuration_error',
|
|
29
|
-
},
|
|
30
|
-
}, { status: 501 });
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const ttsRes = await fetch(OPENAI_TTS_URL, {
|
|
34
|
-
method: 'POST',
|
|
35
|
-
headers: {
|
|
36
|
-
'Authorization': `Bearer ${openaiKey}`,
|
|
37
|
-
'Content-Type': 'application/json',
|
|
38
|
-
},
|
|
39
|
-
body: JSON.stringify({
|
|
40
|
-
model: model === 'claude-audio-4' ? 'tts-1' : model,
|
|
41
|
-
input: input.slice(0, 8000),
|
|
42
|
-
voice,
|
|
43
|
-
response_format: response_format === 'wav' ? 'wav' : 'mp3',
|
|
44
|
-
speed: Math.min(Math.max(speed ?? 1.0, 0.25), 4.0),
|
|
45
|
-
}),
|
|
46
|
-
signal: AbortSignal.timeout(30000),
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
if (!ttsRes.ok) {
|
|
50
|
-
const err = await ttsRes.json().catch(() => ({}));
|
|
51
|
-
return NextResponse.json({ error: { message: (err as any)?.error?.message ?? 'TTS provider error', type: 'upstream_error' } }, { status: 502 });
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Count as 1 request
|
|
55
|
-
await prisma.apiKey.update({
|
|
56
|
-
where: { id: key.id },
|
|
57
|
-
data: { lastUsedAt: new Date(), windowRequestsUsed: { increment: 1 } },
|
|
58
|
-
}).catch(() => {});
|
|
59
|
-
|
|
60
|
-
const audioBuffer = await ttsRes.arrayBuffer();
|
|
61
|
-
const contentType = ttsRes.headers.get('content-type') ?? 'audio/mpeg';
|
|
62
|
-
|
|
63
|
-
return new Response(audioBuffer, {
|
|
64
|
-
headers: {
|
|
65
|
-
'Content-Type': contentType,
|
|
66
|
-
'Content-Length': String(audioBuffer.byteLength),
|
|
67
|
-
},
|
|
68
|
-
});
|
|
69
|
-
} catch (err) {
|
|
70
|
-
console.error('[ClaudMax Audio error]:', err);
|
|
71
|
-
return NextResponse.json({ error: { message: 'Internal server error', type: 'internal_error' } }, { status: 500 });
|
|
72
|
-
}
|
|
73
|
-
}
|