-
Notifications
You must be signed in to change notification settings - Fork 29
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
c57c9b1
commit d14d998
Showing
224 changed files
with
28,980 additions
and
108 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
name: Docker Nightly Image CI | ||
|
||
on: | ||
schedule: | ||
- cron: '0 1 * * *' | ||
workflow_dispatch: | ||
|
||
jobs: | ||
push_to_registry: | ||
name: Push Docker image to Docker Hub | ||
runs-on: ubuntu-latest | ||
steps: | ||
- name: Check out the repo | ||
uses: actions/checkout@v3 | ||
- name: Log in to Docker Hub | ||
uses: docker/login-action@v2 | ||
with: | ||
username: ${{ secrets.DOCKER_USERNAME }} | ||
password: ${{ secrets.DOCKER_PASSWORD }} | ||
|
||
- name: Extract metadata (tags, labels) for Docker | ||
id: meta | ||
uses: docker/metadata-action@v4 | ||
with: | ||
images: gosuto/chatgpt-next-web-langchain | ||
tags: | | ||
type=raw,value=latest | ||
type=ref,event=tag | ||
- name: Set up QEMU | ||
uses: docker/setup-qemu-action@v2 | ||
|
||
- name: Set up Docker Buildx | ||
uses: docker/setup-buildx-action@v2 | ||
|
||
- name: Build and push Docker image | ||
uses: docker/build-push-action@v4 | ||
with: | ||
context: . | ||
platforms: linux/amd64,linux/arm64 | ||
push: true | ||
tags: ${{ secrets.DOCKER_USERNAME }}/chatgpt-next-web-langchain:${{ github.event.inputs.tag || 'nightly' }} | ||
labels: ${{ steps.meta.outputs.labels }} | ||
cache-from: type=gha | ||
cache-to: type=gha,mode=max |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
import { NextRequest } from "next/server"; | ||
import { getServerSideConfig } from "../config/server"; | ||
import md5 from "spark-md5"; | ||
import { ACCESS_CODE_PREFIX, ModelProvider } from "../constant"; | ||
|
||
function getIP(req: NextRequest) { | ||
let ip = req.ip ?? req.headers.get("x-real-ip"); | ||
const forwardedFor = req.headers.get("x-forwarded-for"); | ||
|
||
if (!ip && forwardedFor) { | ||
ip = forwardedFor.split(",").at(0) ?? ""; | ||
} | ||
|
||
return ip; | ||
} | ||
|
||
function parseApiKey(bearToken: string) { | ||
const token = bearToken.trim().replaceAll("Bearer ", "").trim(); | ||
const isApiKey = !token.startsWith(ACCESS_CODE_PREFIX); | ||
|
||
return { | ||
accessCode: isApiKey ? "" : token.slice(ACCESS_CODE_PREFIX.length), | ||
apiKey: isApiKey ? token : "", | ||
}; | ||
} | ||
|
||
export function auth(req: NextRequest, modelProvider: ModelProvider) { | ||
const authToken = req.headers.get("Authorization") ?? ""; | ||
|
||
// check if it is openai api key or user token | ||
let { accessCode, apiKey } = parseApiKey(authToken); | ||
|
||
if (modelProvider === ModelProvider.GeminiPro) { | ||
const googleAuthToken = req.headers.get("x-goog-api-key") ?? ""; | ||
apiKey = googleAuthToken.trim().replaceAll("Bearer ", "").trim(); | ||
} | ||
|
||
const hashedCode = md5.hash(accessCode ?? "").trim(); | ||
|
||
const serverConfig = getServerSideConfig(); | ||
console.log("[Auth] allowed hashed codes: ", [...serverConfig.codes]); | ||
console.log("[Auth] got access code:", accessCode); | ||
console.log("[Auth] hashed access code:", hashedCode); | ||
console.log("[User IP] ", getIP(req)); | ||
console.log("[Time] ", new Date().toLocaleString()); | ||
|
||
if (serverConfig.needCode && !serverConfig.codes.has(hashedCode) && !apiKey) { | ||
return { | ||
error: true, | ||
msg: !accessCode ? "empty access code" : "wrong access code", | ||
}; | ||
} | ||
|
||
if (serverConfig.hideUserApiKey && !!apiKey) { | ||
return { | ||
error: true, | ||
msg: "you are not allowed to access with your own api key", | ||
}; | ||
} | ||
|
||
// if user does not provide an api key, inject system api key | ||
if (!apiKey) { | ||
const serverConfig = getServerSideConfig(); | ||
|
||
const systemApiKey = | ||
modelProvider === ModelProvider.GeminiPro | ||
? serverConfig.googleApiKey | ||
: serverConfig.isAzure | ||
? serverConfig.azureApiKey | ||
: serverConfig.apiKey; | ||
if (systemApiKey) { | ||
console.log("[Auth] use system api key"); | ||
req.headers.set("Authorization", `Bearer ${systemApiKey}`); | ||
} else { | ||
console.log("[Auth] admin did not provide an api key"); | ||
} | ||
} else { | ||
console.log("[Auth] use user api key"); | ||
} | ||
|
||
return { | ||
error: false, | ||
}; | ||
} | ||
|
||
export function googleAuth(req: NextRequest) { | ||
const authToken = req.headers.get("Authorization") ?? ""; | ||
|
||
// check if it is openai api key or user token | ||
const { accessCode, apiKey } = parseApiKey(authToken); | ||
|
||
const hashedCode = md5.hash(accessCode ?? "").trim(); | ||
|
||
const serverConfig = getServerSideConfig(); | ||
console.log("[Auth] allowed hashed codes: ", [...serverConfig.codes]); | ||
console.log("[Auth] got access code:", accessCode); | ||
console.log("[Auth] hashed access code:", hashedCode); | ||
console.log("[User IP] ", getIP(req)); | ||
console.log("[Time] ", new Date().toLocaleString()); | ||
|
||
if (serverConfig.needCode && !serverConfig.codes.has(hashedCode) && !apiKey) { | ||
return { | ||
error: true, | ||
msg: !accessCode ? "empty access code" : "wrong access code", | ||
}; | ||
} | ||
|
||
if (serverConfig.hideUserApiKey && !!apiKey) { | ||
return { | ||
error: true, | ||
msg: "you are not allowed to access openai with your own api key", | ||
}; | ||
} | ||
|
||
// if user does not provide an api key, inject system api key | ||
if (!apiKey) { | ||
const serverApiKey = serverConfig.googleApiKey; | ||
|
||
if (serverApiKey) { | ||
console.log("[Auth] use system api key"); | ||
req.headers.set( | ||
"Authorization", | ||
`${serverConfig.isAzure ? "" : "Bearer "}${serverApiKey}`, | ||
); | ||
} else { | ||
console.log("[Auth] admin did not provide an api key"); | ||
} | ||
} else { | ||
console.log("[Auth] use user api key"); | ||
} | ||
|
||
return { | ||
error: false, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
import { NextRequest, NextResponse } from "next/server"; | ||
import { getServerSideConfig } from "../config/server"; | ||
import { DEFAULT_MODELS, OPENAI_BASE_URL, GEMINI_BASE_URL } from "../constant"; | ||
import { collectModelTable } from "../utils/model"; | ||
import { makeAzurePath } from "../azure"; | ||
|
||
const serverConfig = getServerSideConfig(); | ||
|
||
export async function requestOpenai(req: NextRequest) { | ||
const controller = new AbortController(); | ||
|
||
var authValue, | ||
authHeaderName = ""; | ||
if (serverConfig.isAzure) { | ||
authValue = | ||
req.headers | ||
.get("Authorization") | ||
?.trim() | ||
.replaceAll("Bearer ", "") | ||
.trim() ?? ""; | ||
|
||
authHeaderName = "api-key"; | ||
} else { | ||
authValue = req.headers.get("Authorization") ?? ""; | ||
authHeaderName = "Authorization"; | ||
} | ||
|
||
let path = `${req.nextUrl.pathname}${req.nextUrl.search}`.replaceAll( | ||
"/api/openai/", | ||
"", | ||
); | ||
|
||
let baseUrl = | ||
serverConfig.azureUrl || serverConfig.baseUrl || OPENAI_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); | ||
// this fix [Org ID] undefined in server side if not using custom point | ||
if (serverConfig.openaiOrgId !== undefined) { | ||
console.log("[Org ID]", serverConfig.openaiOrgId); | ||
} | ||
|
||
const timeoutId = setTimeout( | ||
() => { | ||
controller.abort(); | ||
}, | ||
10 * 60 * 1000, | ||
); | ||
|
||
if (serverConfig.isAzure) { | ||
if (!serverConfig.azureApiVersion) { | ||
return NextResponse.json({ | ||
error: true, | ||
message: `missing AZURE_API_VERSION in server env vars`, | ||
}); | ||
} | ||
path = makeAzurePath(path, serverConfig.azureApiVersion); | ||
} | ||
|
||
const clonedBody = await req.text(); | ||
const jsonBody = JSON.parse(clonedBody) as { model?: string }; | ||
if (serverConfig.isAzure) { | ||
baseUrl = `${baseUrl}/${jsonBody.model}`; | ||
} | ||
const fetchUrl = `${baseUrl}/${path}`; | ||
const fetchOptions: RequestInit = { | ||
headers: { | ||
"Content-Type": "application/json", | ||
"Cache-Control": "no-store", | ||
[authHeaderName]: authValue, | ||
...(serverConfig.openaiOrgId && { | ||
"OpenAI-Organization": serverConfig.openaiOrgId, | ||
}), | ||
}, | ||
method: req.method, | ||
body: clonedBody, | ||
// to fix #2485: https://stackoverflow.com/questions/55920957/cloudflare-worker-typeerror-one-time-use-body | ||
redirect: "manual", | ||
// @ts-ignore | ||
duplex: "half", | ||
signal: controller.signal, | ||
}; | ||
|
||
// #1815 try to refuse gpt4 request | ||
if (serverConfig.customModels && clonedBody) { | ||
try { | ||
const modelTable = collectModelTable( | ||
DEFAULT_MODELS, | ||
serverConfig.customModels, | ||
); | ||
// const clonedBody = await req.text(); | ||
// const jsonBody = JSON.parse(clonedBody) as { model?: string }; | ||
fetchOptions.body = clonedBody; | ||
|
||
// not undefined and is false | ||
if (modelTable[jsonBody?.model ?? ""].available === false) { | ||
return NextResponse.json( | ||
{ | ||
error: true, | ||
message: `you are not allowed to use ${jsonBody?.model} model`, | ||
}, | ||
{ | ||
status: 403, | ||
}, | ||
); | ||
} | ||
} catch (e) { | ||
console.error("[OpenAI] gpt4 filter", e); | ||
} | ||
} | ||
|
||
try { | ||
const res = await fetch(fetchUrl, fetchOptions); | ||
|
||
// 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"); | ||
|
||
// The latest version of the OpenAI API forced the content-encoding to be "br" in json response | ||
// So if the streaming is disabled, we need to remove the content-encoding header | ||
// Because Vercel uses gzip to compress the response, if we don't remove the content-encoding header | ||
// The browser will try to decode the response with brotli and fail | ||
newHeaders.delete("content-encoding"); | ||
|
||
return new Response(res.body, { | ||
status: res.status, | ||
statusText: res.statusText, | ||
headers: newHeaders, | ||
}); | ||
} finally { | ||
clearTimeout(timeoutId); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import { NextResponse } from "next/server"; | ||
|
||
import { getServerSideConfig } from "../../config/server"; | ||
|
||
const serverConfig = getServerSideConfig(); | ||
|
||
// Danger! Do not hard code any secret value here! | ||
// 警告!不要在这里写入任何敏感信息! | ||
const DANGER_CONFIG = { | ||
needCode: serverConfig.needCode, | ||
hideUserApiKey: serverConfig.hideUserApiKey, | ||
disableGPT4: serverConfig.disableGPT4, | ||
hideBalanceQuery: serverConfig.hideBalanceQuery, | ||
disableFastLink: serverConfig.disableFastLink, | ||
customModels: serverConfig.customModels, | ||
}; | ||
|
||
declare global { | ||
type DangerConfig = typeof DANGER_CONFIG; | ||
} | ||
|
||
async function handle() { | ||
return NextResponse.json(DANGER_CONFIG); | ||
} | ||
|
||
export const GET = handle; | ||
export const POST = handle; | ||
|
||
export const runtime = "edge"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import { NextRequest, NextResponse } from "next/server"; | ||
|
||
async function handle( | ||
req: NextRequest, | ||
{ params }: { params: { path: string[] } }, | ||
) { | ||
if (req.method === "OPTIONS") { | ||
return NextResponse.json({ body: "OK" }, { status: 200 }); | ||
} | ||
|
||
const [protocol, ...subpath] = params.path; | ||
const targetUrl = `${protocol}://${subpath.join("/")}`; | ||
|
||
const method = req.headers.get("method") ?? undefined; | ||
const shouldNotHaveBody = ["get", "head"].includes( | ||
method?.toLowerCase() ?? "", | ||
); | ||
|
||
const fetchOptions: RequestInit = { | ||
headers: { | ||
authorization: req.headers.get("authorization") ?? "", | ||
}, | ||
body: shouldNotHaveBody ? null : req.body, | ||
method, | ||
// @ts-ignore | ||
duplex: "half", | ||
}; | ||
|
||
const fetchResult = await fetch(targetUrl, fetchOptions); | ||
|
||
console.log("[Any Proxy]", targetUrl, { | ||
status: fetchResult.status, | ||
statusText: fetchResult.statusText, | ||
}); | ||
|
||
return fetchResult; | ||
} | ||
|
||
export const POST = handle; | ||
export const GET = handle; | ||
export const OPTIONS = handle; | ||
|
||
export const runtime = "nodejs"; |
Oops, something went wrong.