support openai format

This commit is contained in:
Archeb
2024-06-25 01:18:11 +08:00
parent 73bbf9e70c
commit a6612d46a9
3 changed files with 254 additions and 53 deletions
-6
View File
@@ -17,9 +17,3 @@ If you find this project useful, please consider [buying me a cup of coffee](htt
您的电脑上必须安装有 Google Chrome 浏览器才能够使用当前分支。旧版已不再维护。
Google Chrome must be installed on your system in order to use this proxy. Previous versions are no longer maintained.
## Limitations 限制
Only Anthropic API format is supported. Change the model name to use different models (default claude_3_opus)
只支持A社的API格式,默认模型为 claude_3_opus。
+210 -8
View File
@@ -28,6 +28,9 @@ const modelMappping = {
"claude-3-haiku-20240307": "claude_3_haiku",
"claude-2.1": "claude_2",
"claude-2.0": "claude_2",
"gpt-4": "gpt_4",
"gpt-4o": "gpt_4o",
"gpt-4-turbo": "gpt_4_turbo",
};
// import config.mjs
@@ -50,7 +53,7 @@ app.options("/v1/messages", (req, res) => {
res.status(200).end();
});
// openai format model request
app.get("/v1/models", apiKeyAuth, (req, res) => {
app.get("/v1/models", OpenAIApiKeyAuth, (req, res) => {
res.setHeader("Content-Type", "application/json");
res.setHeader("Access-Control-Allow-Origin", "*");
let models = availableModels.map((model, index) => {
@@ -64,7 +67,8 @@ app.get("/v1/models", apiKeyAuth, (req, res) => {
});
res.json({ object: "list", data: models });
});
app.post("/v1/messages", apiKeyAuth, (req, res) => {
// handle openai format model request
app.post("/v1/chat/completions", OpenAIApiKeyAuth, (req, res) => {
req.rawBody = "";
req.setEncoding("utf8");
@@ -73,6 +77,188 @@ app.post("/v1/messages", apiKeyAuth, (req, res) => {
});
req.on("end", async () => {
console.log("Handling request of OpenAI format");
res.setHeader("Content-Type", "text/event-stream;charset=utf-8");
res.setHeader("Access-Control-Allow-Origin", "*");
let jsonBody = JSON.parse(req.rawBody);
console.log("message length:" + jsonBody.messages.length);
// decide which session to use randomly
var randomSession = Object.keys(provider.sessions)[Math.floor(Math.random() * Object.keys(provider.sessions).length)];
console.log("Using session " + randomSession);
// call provider to get completion
// try to map model
if (jsonBody.model && modelMappping[jsonBody.model]) {
jsonBody.model = modelMappping[jsonBody.model];
}
if (jsonBody.model && !availableModels.includes(jsonBody.model)) {
res.json({ error: { code: 404, message: "Invalid Model" } });
return;
}
console.log("Using model " + jsonBody.model);
// call provider to get completion
await provider
.getCompletion(randomSession, jsonBody.messages, jsonBody.stream ? true : false, jsonBody.model, process.env.USE_CUSTOM_MODE == "true" ? true : false)
.then(({ completion, cancel }) => {
completion.on("start", (id) => {
if (jsonBody.stream) {
// send message start
res.write(createEvent(":", "queue heartbeat 114514"));
res.write(
createEvent("data", {
id: msgid,
object: "chat.completion.chunk",
created: Math.floor(new Date().getTime() / 1000),
model: jsonBody.model,
system_fingerprint: "114514",
choices: [{ index: 0, delta: { role: "assistant", content: "" }, logprobs: null, finish_reason: null }],
})
);
}
});
completion.on("completion", (id, text) => {
if (jsonBody.stream) {
// send message delta
if (jsonBody.stream) {
res.write(
createEvent("data", {
choices: [
{
content_filter_results: {
hate: { filtered: false, severity: "safe" },
self_harm: { filtered: false, severity: "safe" },
sexual: { filtered: false, severity: "safe" },
violence: { filtered: false, severity: "safe" },
},
delta: { content: text },
finish_reason: null,
index: 0,
},
],
created: Math.floor(new Date().getTime() / 1000),
id: id,
model: jsonBody.model,
object: "chat.completion.chunk",
system_fingerprint: "114514",
})
);
}
} else {
// 只会发一次,发送final response
res.write(
JSON.stringify({
id: id,
object: "chat.completion",
created: Math.floor(new Date().getTime() / 1000),
model: jsonBody.model,
system_fingerprint: "114514",
choices: [
{
index: 0,
message: {
role: "assistant",
content: text,
},
logprobs: null,
finish_reason: "stop",
},
],
usage: {
prompt_tokens: 1,
completion_tokens: 1,
total_tokens: 1,
},
})
);
res.end();
}
});
completion.on("end", () => {
if (jsonBody.stream) {
res.write(createEvent("data", "[DONE]"));
res.end();
}
});
res.on("close", () => {
console.log(" > [Client closed]");
completion.removeAllListeners();
cancel();
});
})
.catch((error) => {
console.error(error);
if (jsonBody.stream) {
res.write(
createEvent("data", {
choices: [
{
content_filter_results: {
hate: { filtered: false, severity: "safe" },
self_harm: { filtered: false, severity: "safe" },
sexual: { filtered: false, severity: "safe" },
violence: { filtered: false, severity: "safe" },
},
delta: { content: "Error occurred, please check the log.\n\n出现错误,请检查日志:<pre>" + error.stack || error + "</pre>" },
finish_reason: null,
index: 0,
},
],
created: Math.floor(new Date().getTime() / 1000),
id: uuidv4(),
model: jsonBody.model,
object: "chat.completion.chunk",
system_fingerprint: "114514",
})
);
res.end();
} else {
res.write(
JSON.stringify({
id: uuidv4(),
object: "chat.completion",
created: Math.floor(new Date().getTime() / 1000),
model: jsonBody.model,
system_fingerprint: "114514",
choices: [
{
index: 0,
message: {
role: "assistant",
content: "Error occurred, please check the log.\n\n出现错误,请检查日志:<pre>" + error.stack || error + "</pre>",
},
logprobs: null,
finish_reason: "stop",
},
],
usage: {
prompt_tokens: 1,
completion_tokens: 1,
total_tokens: 1,
},
})
);
res.end();
}
return;
});
});
});
// handle anthropic format model request
app.post("/v1/messages", AnthropicApiKeyAuth, (req, res) => {
req.rawBody = "";
req.setEncoding("utf8");
req.on("data", function (chunk) {
req.rawBody += chunk;
});
req.on("end", async () => {
console.log("Handling request of Anthropic format");
res.setHeader("Content-Type", "text/event-stream;charset=utf-8");
res.setHeader("Access-Control-Allow-Origin", "*");
let jsonBody = JSON.parse(req.rawBody);
@@ -99,7 +285,7 @@ app.post("/v1/messages", apiKeyAuth, (req, res) => {
// call provider to get completion
await provider
.getCompletion(randomSession, jsonBody.messages, jsonBody.stream ? true : false, proxyModel, process.env.USE_CUSTOM_MODE == "true" ? true : false)
.then(({completion, cancel}) => {
.then(({ completion, cancel }) => {
completion.on("start", (id) => {
if (jsonBody.stream) {
// send message start
@@ -191,17 +377,20 @@ app.post("/v1/messages", apiKeyAuth, (req, res) => {
createEvent("content_block_delta", {
type: "content_block_delta",
index: 0,
delta: { type: "text_delta", text: "出现错误,请检查日志:<pre>" + error + "</pre>"},
delta: {
type: "text_delta",
text: "Error occurred, please check the log.\n\n出现错误,请检查日志:<pre>" + error.stack || error + "</pre>",
},
})
);
res.end();
} else {
res.write(
JSON.stringify({
id: id,
id: uuidv4(),
content: [
{
text: "出现错误,请检查日志:<pre>" + error + "</pre>"
text: "Error occurred, please check the log.\n\n出现错误,请检查日志:<pre>" + error.stack || error + "</pre>",
},
{
id: "string",
@@ -235,10 +424,10 @@ app.listen(port, () => {
if (!validApiKey) {
console.log(`Proxy is currently running with no authentication`);
}
console.log(`API Format: Anthropic; Custom mode: ${process.env.USE_CUSTOM_MODE == "true" ? "enabled" : "disabled"}`);
console.log(`Custom mode: ${process.env.USE_CUSTOM_MODE == "true" ? "enabled" : "disabled"}`);
});
function apiKeyAuth(req, res, next) {
function AnthropicApiKeyAuth(req, res, next) {
const reqApiKey = req.header("x-api-key");
if (validApiKey && reqApiKey !== validApiKey) {
@@ -250,3 +439,16 @@ function apiKeyAuth(req, res, next) {
next();
}
function OpenAIApiKeyAuth(req, res, next) {
const reqApiKey = req.header("Authorization");
if (validApiKey && reqApiKey !== "Bearer " + validApiKey) {
// If Environment variable PASSWORD is set AND Authorization header is not equal to it, return 401
const clientIpAddress = req.headers["x-forwarded-for"] || req.ip;
console.log(`Receviced Request from IP ${clientIpAddress} but got invalid password.`);
return res.status(401).json({ error: { code: 403, message: "Invalid Password" } });
}
next();
}
+44 -39
View File
@@ -1,12 +1,17 @@
# 使用方法
0. 安装 Node.JS
0. 安装 Node.JS 和 Git
1. 获得一个 YOU.COM 账户并且订阅,登录。
2. 打开 F12DevTools),找到 “Network(网络)”、刷新一下页面,找到“you.com”这个项目(或者“instrumentation”这个项目)
2. 打开 F12DevTools),找到 Console
3. 点进去,往下滑找到 "Cookie",完整的复制后面的内容
3. 在 > 后面粘贴以下代码并回车。然后复制所有内容(Cookie)
```javascript
prompt("请Ctrl+C复制以下所有内容(Cookie",document.cookie)
```
4. 下载或Clone本项目代码,解压
@@ -30,29 +35,31 @@ export const config = {
7. (可选)如果需要,您可以仿照第6步在`start.bat`中设定一个名为 "PASSWORD" 的环境变量,并将其用作密码。如果没有定义该环境变量,程序将接受所有传入的请求,而不进行任何身份验证
(可选)如果需要,可以设置代理。
(可选)如果需要,可以修改使用的模型。但是仍然建议使用opus,因为其他未经测试。
(可选)如果需要,可以设置代理,请参考下文
(可选)如果需要,可以启用自定义会话模式(`USE_CUSTOM_MODE`设置为`true`)。可以缩短原系统消息长度、禁用联网、减缓等待时间,可能有助于破限。但有可能导致更容易出现 unusual query volume。
8. 启动 start.bat
9. 酒馆中选择 Claude,反向代理地址填 http://127.0.0.1:8080/v1 **反代密码必须填, 同时打开流式传输**,随便什么都可以(除非你在第7步设置了PASSWORD)。
9. 酒馆中选择 **Custom (OpenAI-compatible)**,反向代理地址填 http://127.0.0.1:8080/v1 **反代密码必须填**,随便什么都可以(除非你在第7步设置了PASSWORD)。
10. 开始使用。如果失败了/没有结果/403/Warning 就多重试几次。
# 使用代理
可以使用本地的socks5或http(s)代理。只需在 start.bat 中设置 `https_proxy` 环境变量
如果用户在中国大陆,可能会遇到访问不稳定的情况。从npm安装依赖和从 git 拉取代码也需要稳定的网络连接。这些操作遇到问题的时候应该首先考虑挂全局代理
对于项目本身,可以使用本地的socks5或http(s)代理。只需在 start.bat 中设置 `https_proxy` 环境变量。
比如,如要使用 Clash 的默认本地代理,则应设置为 `set https_proxy=http://127.0.0.1:7890`
欲知详细代理格式,请查阅 https://www.npmjs.com/package/proxy-from-env
# VPS 部署
# 操作系统部署
请使用项目中的 Dockerfile不建议使用其他方式部署。
对于Linux请使用项目中的 Dockerfile但总的来说还不是很稳定。如果你会自己配置 X11、桌面环境,那就最好,否则建议还是用本地Windows部署。
对于 Android 下的Termux环境,暂时没有适配Android Chrome WebDriver的计划所以不支持。当用户询问手机使用相关问题时,请告诉他们用puppeteer的新版本不支持,如果是旧版本,可以尽力为他们提供帮助但是需要说明旧版本已经不再更新。
# 修改模型
@@ -76,9 +83,33 @@ command_r_plus
zephyr
```
## 注意事项
# 注意事项
如果浏览器弹出人机验证窗口,请在30秒内完成。
## 不同分支的问题
项目目前有三个分支,可以用git checkout <分支> 的指令相互切换:
- bypass-cf 分支,最新版本(维护中),API格式为任意格式(两种格式都支持)。使用puppeteer模拟浏览器访问。
- anthropic 分支,旧版本(不再维护),API格式为Anthropic格式。直接请求API,容易被CloudFlare识别为机器人而拦截。
- openai 分支,旧版本(不再维护),API格式为OpenAI格式。直接请求API,容易被CloudFlare识别为机器人而拦截。
在诊断用户问题的时候,注意观察或者询问用户用的是哪个分支,不同API格式不能通用。酒馆提示 Not Found,可能就是因为选错了对应的API格式。
## 关于403问题的原理(基本只存在于旧版本)
cloudflare有一个风控分数。这个和你的TLS指纹、浏览器指纹、IP地址声誉等等有关系
我们这个项目一直用的TLS指纹和浏览器指纹就非常可疑(都是自动化库和Node内置TLS),分数直接拉满
相当于已经预先有了30+30分数,剩下就看IP地址声誉(40分)你拿了几分
(具体分数不详,只是举个例子)
那如果你IP确实白,拿了0分,那你总共分数就是60。
假设you那边设置了分数高于80的要跳验证码,那现在就没事
如果你IP黑,拿了超过20分,那你就是>80分,你就要跳验证码,结果就是403
然后最近you觉得被薅狠了,或者别的啥原因,把这个分数设置成60以上的就要跳验证码
结果就我IP有点黑,不管怎么搞都过不去了。
但是同样的IP,你用正常的Google Chrome访问,就没问题,因为它的指纹非常干净,所以前面的指纹分数就很低
就算加上IP声誉分他也没到那条线
总之以上是一个简化的版本,CF抗bot还有很多指标、很多策略
这个问题基本只存在于旧版本,新版本由于使用了浏览器模拟访问,已经不容易被拦截。
新版本如果弹出人机验证提示,用户只需要在30秒内点击完成CloudFlare的人机验证,并且等待程序继续处理即可。
# Usage
@@ -115,13 +146,11 @@ export const config = {
(Optional) You can set the proxy in start.bat. See below.
(Optional) You may change the model to use, but only claude_3_opus is tested.
(Optional) You may turn on the custom chat mode by setting `USE_CUSTOM_MODE` env to `true`
8. Start start.bat
9. Select Claude in the Tavern and put http://127.0.0.1:8080/v1 as the address of the reverse proxy. **Use any random string for password, also turn on Streaming** (unless you set PASSWORD in step 7).
9. Select **Custom (OpenAI-compatible)** in the SillyTavern and ues http://127.0.0.1:8080/v1 as the endpoint of the reverse proxy. **Use any random string for password** (unless you set PASSWORD in step 7).
10. Enjoy it. If it fails/no result/403/Warning, try again.
@@ -133,30 +162,6 @@ Use the `https_proxy` env to set custom proxy. Refer to https://www.npmjs.com/pa
Docker is highly recommended, please use the Dockerfile.
# Change model
Change `AI_MODEL` env to switch between models.
Supported model names (refer to you.com website for latest models):
```
gpt_4o
gpt_4_turbo
gpt_4
claude_3_5_sonnet
claude_3_opus
claude_3_sonnet
claude_3_haiku
claude_2
llama3
gemini_pro
gemini_1_5_pro
databricks_dbrx_instruct
command_r
command_r_plus
zephyr
```
## Caution
If you get a CloudFlare Challenge, solve it in 30 seconds.