ChatGPT-Next-Web/app/api/anthropic.ts

172 lines
4.3 KiB
TypeScript

import type { NextRequest } from 'next/server';
import { getServerSideConfig } from '@/app/config/server';
import {
Anthropic,
ANTHROPIC_BASE_URL,
ApiPath,
ModelProvider,
ServiceProvider,
} from '@/app/constant';
import { cloudflareAIGatewayUrl } from '@/app/utils/cloudflare';
import { prettyObject } from '@/app/utils/format';
import { isModelAvailableInServer } from '@/app/utils/model';
import { NextResponse } from 'next/server';
import { auth } from './auth';
const ALLOWD_PATH = new Set([Anthropic.ChatPath, Anthropic.ChatPath1]);
export async function handle(
req: NextRequest,
{ params }: { params: { path: string[] } },
) {
console.log('[Anthropic Route] params ', params);
if (req.method === 'OPTIONS') {
return NextResponse.json({ body: 'OK' }, { status: 200 });
}
const subpath = params.path.join('/');
if (!ALLOWD_PATH.has(subpath)) {
console.log('[Anthropic Route] forbidden path ', subpath);
return NextResponse.json(
{
error: true,
msg: `you are not allowed to request ${subpath}`,
},
{
status: 403,
},
);
}
const authResult = auth(req, ModelProvider.Claude);
if (authResult.error) {
return NextResponse.json(authResult, {
status: 401,
});
}
try {
const response = await request(req);
return response;
} catch (e) {
console.error('[Anthropic] ', e);
return NextResponse.json(prettyObject(e));
}
}
const serverConfig = getServerSideConfig();
async function request(req: NextRequest) {
const controller = new AbortController();
const authHeaderName = 'x-api-key';
const authValue
= req.headers.get(authHeaderName)
|| req.headers.get('Authorization')?.replaceAll('Bearer ', '').trim()
|| serverConfig.anthropicApiKey
|| '';
const path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Anthropic, '');
let baseUrl
= serverConfig.anthropicUrl || serverConfig.baseUrl || ANTHROPIC_BASE_URL;
if (!baseUrl.startsWith('http')) {
baseUrl = `https://${baseUrl}`;
}
if (baseUrl.endsWith('/')) {
baseUrl = baseUrl.slice(0, -1);
}
console.log('[Proxy] ', path);
console.log('[Base Url]', baseUrl);
const timeoutId = setTimeout(
() => {
controller.abort();
},
10 * 60 * 1000,
);
// try rebuild url, when using cloudflare ai gateway in server
const fetchUrl = cloudflareAIGatewayUrl(`${baseUrl}${path}`);
const fetchOptions: RequestInit = {
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'no-store',
'anthropic-dangerous-direct-browser-access': 'true',
[authHeaderName]: authValue,
'anthropic-version':
req.headers.get('anthropic-version')
|| serverConfig.anthropicApiVersion
|| Anthropic.Vision,
},
method: req.method,
body: req.body,
redirect: 'manual',
// @ts-ignore
duplex: 'half',
signal: controller.signal,
};
// #1815 try to refuse some request to some models
if (serverConfig.customModels && req.body) {
try {
const clonedBody = await req.text();
fetchOptions.body = clonedBody;
const jsonBody = JSON.parse(clonedBody) as { model?: string };
// not undefined and is false
if (
isModelAvailableInServer(
serverConfig.customModels,
jsonBody?.model as string,
ServiceProvider.Anthropic as string,
)
) {
return NextResponse.json(
{
error: true,
message: `you are not allowed to use ${jsonBody?.model} model`,
},
{
status: 403,
},
);
}
} catch (e) {
console.error(`[Anthropic] filter`, e);
}
}
// console.log("[Anthropic request]", fetchOptions.headers, req.method);
try {
const res = await fetch(fetchUrl, fetchOptions);
// console.log(
// "[Anthropic response]",
// res.status,
// " ",
// res.headers,
// res.url,
// );
// to prevent browser prompt for credentials
const newHeaders = new Headers(res.headers);
newHeaders.delete('www-authenticate');
// to disable nginx buffering
newHeaders.set('X-Accel-Buffering', 'no');
return new Response(res.body, {
status: res.status,
statusText: res.statusText,
headers: newHeaders,
});
} finally {
clearTimeout(timeoutId);
}
}