大模型逆向工程入门

看大家用我的 4o-free 项目还挺开心的,想着授人以鱼不如授人以渔,出个简单的逆向教学(

# 前言

本教程需要有一定Python基础,本教程作者在Python也不算是专家,仅作为参考用
原创,首发NodeLoc,搬运通知&标出处

# 0x00 确定目标

目标最好选定那种新创公司(草台班子),这种一般都做的防护不好/没有防护,逆向轻而易举
如果遇见那种需要一堆验证的/要经常登录的/每次访问都弹CF的,建议直接放弃
(偶尔弹CF还是可以尝试)

本教程将以 https://ai-chats.org/chat/ 做示例。

# 0x01 分析请求

在网站上发起一个聊天请求,并通过Chrome(或者其他浏览器)的Devtools抓取这个请求。
(请求的Path一般都很有辨识性,如`completions`或`send`)
[2024-08-10-22-34-34.png![2024-08-10-22-34-34.png](https://i.postimg.cc/L4z30NHq/2024-08-10-22-34-34.png)](https://postimg.cc/mhg9P31R)

接着查看“载荷”选项卡,分析请求的Payload。

```json { "type": "chat", "messagesHistory": [ { "id": "9025c8e5-15a3-4ef3-b299-5eff7e077cb9", "from": "you", "content": "你好啊!" } ] } ```

很幸运,ai-chats.org的结构非常简单,我们可以看见上下文就是被包装在`messagesHistory`字段中。
接下来,我们查看多轮对话的情况。

```json { "type": "chat", "messagesHistory": [ { "id": "9025c8e5-15a3-4ef3-b299-5eff7e077cb9", "from": "you", "content": "你好啊!" }, { "id": "b0eb1255-3adc-439e-a0e7-abc1e9f5b4e7", "from": "chatGPT", "content": "你好!有什么我可以帮你的吗?" }, { "id": "23b15e51-527c-4d4d-90b3-935f9742cbfb", "from": "you", "content": "你是谁?" } ] } ```

我们几乎对对话结构清楚了。对于用户发出的文本,`from`字段是`you`,模型发出的文本,`from`字段是`chatGPT`。
我们依然可以注意到:每个对话元素都有一个UUID,我们目前假设它是随机的,写出一个生成有效载荷的Python函数。

```python import uuid

def generate_chat_payload(messages):
payload = {“type”:“chat”,“messagesHistory”:}
for message in messages:
payload[“messagesHistory”].append({
“id”: str(uuid.uuid4()),
“from”: “chatGPT” if message[“role”] == “assistant” else “you”,
“content”: message[“content”]
})
return payload

```

# 0x02 复现请求

接下来,我们要做的事情就是复现我们在浏览器中发出的请求了。
在请求这块,有一个小细节:HTTP/2请求更不容易被Cloudflare拦截。
但部分原站裸奔/小厂CDN可能不支持HTTP/2,请自行判断。
我们使用优雅的HTTP多版本请求库——`httpx`。

```sh $ # 安装依赖 $ pip3 install httpx httpx[http2] ```

接下来,我们可以使用一个很方便的工具:curlconverter.com.
我们打开Devtools,将刚刚的请求右键,点击“复制为 cURL 格式”,之后粘贴到curlconverter.com,选择Python requests。

```python import requests

cookies = {

}

headers = {

}

json_data = {
‘type’: ‘chat’,
‘messagesHistory’: [
{
‘id’: ‘9025c8e5-15a3-4ef3-b299-5eff7e077cb9’,
‘from’: ‘you’,
‘content’: ‘你好啊!’,
},
],
}

response = requests.post(‘https://ai-chats.org/chat/send2/’, cookies=cookies, headers=headers, json=json_data)
```

这是它生成出来的代码,接下来我们对它进行一些修改,让它变为一个函数,并迁移到httpx。

```python import httpx

def completion():
cookies = {

}

headers = {
    ...
}

json_data = {
    'type': 'chat',
    'messagesHistory': [
        {
            'id': '9025c8e5-15a3-4ef3-b299-5eff7e077cb9',
            'from': 'you',
            'content': '你好啊!',
        },
    ],
}

session = httpx.Client(http2=True)
response = session.post('https://ai-chats.org/chat/send2/', cookies=cookies, headers=headers, json=json_data)
return response

print(completion().text)
```

我们运行脚本,可以看见类似如下的输出:

``` event: trylimit data:

data:

data: 你好

data: !

data: 有什么

data: 我

data: 可以

data: 帮助

data: 你

data: 的吗

data: ?

```

这证明我们的请求成功的被服务器接受了!下一步,我们将会让它对接上我们前面写的生成上下文函数。

# 0x03 进一步适配

我们只需要把这两段代码简单的组合在一起,就能以OpenAI格式的上下文请求ai-chats.org的API了。

```python import uuid import httpx

def generate_chat_payload(messages):
payload = {“type”:“chat”,“messagesHistory”:}
for message in messages:
payload[“messagesHistory”].append({
“id”: str(uuid.uuid4()),
“from”: “chatGPT” if message[“role”] == “assistant” else “you”,
“content”: message[“content”]
})
return payload

def completion(messages):
cookies = {

}

headers = {
    ...
}

json_data = generate_chat_payload(messages)

session = httpx.Client(http2=True)
response = session.post('https://ai-chats.org/chat/send2/', cookies=cookies, headers=headers, json=json_data)
return response<i>

```

到这里,这个逆向出来的API基本就可以用了。但是——我们当然不希望用逆向模型的时候,弹出来一堆上下文的原始数据,不是吗?接下来,我们对completion()函数做一些修改,让其支持EventStream格式。

```python ---snip--- def completion(messages): cookies = { ... }
headers = {
    ...
}

json_data = generate_chat_payload(messages)

client = httpx.Client(http2=True)
with client.stream(
    	"POST", "https://ai-chats.org/chat/send2/", json=json_data
) as request:
	for line in request.iter_lines():
		if line.startswith("data: "):
			yield {"type":"token", "data": line.split("data: ",1)[1]}
yield {"type":"end"}

for i in completion([{“role”:“user”,“content”:“你好!”}]):
print(i)
```

运行脚本,你应该能够看见类似于下面的输出:

``` {'type': 'token', 'data': ''} {'type': 'token', 'data': ''} {'type': 'token', 'data': '你好'} {'type': 'token', 'data': '!'} {'type': 'token', 'data': '有什么'} {'type': 'token', 'data': '我'} {'type': 'token', 'data': '可以'} {'type': 'token', 'data': '帮助'} {'type': 'token', 'data': '你'} {'type': 'token', 'data': '的吗'} {'type': 'token', 'data': '?'} {'type': 'end'} ```

为了方便后面的Eventstream响应,我们还需要一个函数把它包装为OpenAI格式的流:

```python ---snip--- def openai_style_wrapper(messages): created = int(time.time()) compid = "chatcmpl-AiChatsOrg-" + uuid.uuid4().hex for message in completion(messages): if message["type"] == "token": yield "data: " + json.dumps( { "id": compid, "object": "chat.completion.chunk", "created": created, "model": "gpt-4o-mini", "choices": [ { "index": 0, "delta": {"role": "assistant", "content": message["data"]}, "finish_reason": "", } ], } ) + "\n\n" else: yield "data: [DONE]\n\n" ---snip--- ```

这是我们的一大进步!我们把它包装为了一个Python装饰器,还把它的响应转化为了标准的OpenAI格式,以便我们后面把它转换为一个API。

# 0x04 2API,启动!!!

下面一步就是把你的代码适配成一个OpenAI格式的HTTP服务器了。因为这个部分有很多可能性,每个人都有每个人喜欢的框架、技术栈,我就直接贴上我的代码了。

```python import fastapi import fastapi.middleware.cors import httpx import uuid import json import time

AUTHOR = “0x24a”

app = fastapi.FastAPI()

app.add_middleware(
fastapi.middleware.cors.CORSMiddleware,
allow_origins=[““],
allow_credentials=True,
allow_methods=[”
”],
allow_headers=[“*”],
)

—snip—

@app.get(“/v1/models”)
def models():
return {
“data”: [
{“created”: 0, “id”: “gpt-4o-mini”, “object”: “model”, “owned_by”: “0x24a”}
]
}

@app.post(“/v1/chat/completions”)
async def completion(request: fastapi.Request):
data: dict = await request.json()
if data[“model”] != “gpt-4o-mini”:
return {“code”: 500, “text”: “Invaild model”} # only 4o-mini please~
messages = data[“messages”]
streaming = data.get(“stream”, False)
if len(messages) == 0:
return {“code”: 500, “text”: “Invaild message count”}
if not streaming:
content = “”
created = int(time.time())
total_tokens = 0
for i in completion(messages):
if i[“type”] == “token”:
content += i[“data”]
total_tokens += 1
return {
“choices”: [
{
“finish_reason”: “stop”,
“index”: 0,
“message”: {“content”: content, “role”: “assistant”},
“logprobs”: None,
}
],
“created”: created,
“id”: “chatcmpl-AichatsOrg2APIDemo-” + uuid.uuid4().hex,
“model”: “gpt-4o-mini”,
“object”: “chat.completion”,
“usage”: {
“completion_tokens”: total_tokens,
“prompt_tokens”: 0,
“total_tokens”: total_tokens,
},
}
else:
return fastapi.responses.StreamingResponse(
openai_style_wrapper(messages), media_type=“text/event-stream”
)
```

# 0x05 善后

后面我们还需要对模型进行IQ Test,以防模型的能力受到了削弱。

``` You: 请记住,我叫0x24a。 Bot: 好的,0x24a,你有什么需要帮助的吗? You: 我叫什么名字? Bot: 由于我是一个大语言模型,我无法… ```

明显,我们逆向出来的没有上下文(这是因为他们官网就削掉了上下文!),我们可以用Prompt来手搓一个上下文。

```python ---snip--- def generate_chat_payload(messages): payload = {"type":"chat","messagesHistory":[]} generated_context = "For some reason, you've forgotten everything I've ever said to you.\nNext, I'm going to tell you about the conversation I had with you, and ask you to respond to my last sentence.\n" for message in messages: generated_context+=f"<|Message role=\"{message['role']}\"|>{message['content']}</|Message|>" payload["messagesHistory"].append({ "id": str(uuid.uuid4()), "from": "you", "content": generated_context }) return payload ---snip--- ```

我们再次测试上下文功能性:

``` You: 请记住,我叫0x24a。 Bot: 好的,0x24a,你有什么需要帮助的吗? You: 我叫什么名字? Bot: 你叫0x24a。有什么我可以帮助你的吗? ```

# 0x06 结语

目前我们只是完成了一个“能用”的逆向API,对于代理池、多Worker防堵塞、身份验证这些就需要自己研究了。
值得一提的是,4o-free.24a.fun(https://www.nodeloc.com/d/5823)使用的就是这个逆向流程。
教程写的很急、文笔可能很烂……
如文章有错误,欢迎指出!
无论如何,希望你喜欢这篇教程!

牛逼 写得很细节。 我发现爬虫用httpx比request好用的多:huaji09:

@“James”#p64373 是啊,httpx是新时代的HTTP库,一个</s>http2=True<e>就能启用http/2(

最主要的还是cf大善人对http/1.1区别对待

支持。 教程写的很棒,就是PY没有大括弧,适应不了。