私有 AI 和 OpenAI 兼容的 API

现在很多私有化部署的 GPT 正在逐渐变多,且 运算方法在不断的更新,优化也越来越好,使得在笔电上跑一个 GPT 都已经不是问题了。

所以还是有必要记录一下我搭建的私有GPT和对外开放API的坑的。

私有 GPT

方式方法软件都有很多:

OpenAI 的 API 文档

https://platform.openai.com/docs/api-reference/chat/create

ChatGLM(chatglm.cpp)

https://github.com/li-plus/chatglm.cpp

这是一个 c++ 实现的 ChatGLM,效率非常惊人,运行中占用的资源极低。

但是如果想要运行 openai 兼容的 API,有点小坑

1. 安装依赖

issue: cd 到其他目录就不会有 init 冲突问题

# 不要在根目录下安装api的依赖,会有点问题
# No module named 'chatglm_cpp._C' 仓库的最外层目录中 init 文件中引入了这个包,同时 pip 拉取的依赖中的 init 文件 也有这个包,导致冲突。 cd 到其他目录就不会有 init 冲突问题。 至于 pydantic_settings 包报错,可能是拉取依赖的问题
cd chatglm.cpp/chatglm_cp

pip install 'chatglm-cpp[api]'

但是还是会安装失败,尝试启动一下 看缺少什么 手动安装即可

2. 启动:

命令行启动

MODEL=/opt/chatglm.cpp/chatglm3-ggml.bin uvicorn chatglm_cpp.openai_api:app --host 0.0.0.0 --port 9000

systemd

如果不使用root账户运行 则必须在执行命令前手动让用户环境载入一下,否则python的依赖找不到

[Unit]
Description=chatglm cpp
After=syslog.target network.target remote-fs.target nss-lookup.target
 
[Service]
Type=simple
WorkingDirectory=/opt/chatglm.cpp/chatglm_cpp
User=wayne
Group=wayne
Environment="MODEL=/opt/chatglm.cpp/chatglm3-ggml.bin"
ExecStart=/bin/bash --login  -c "source ~/.profile; uvicorn chatglm_cpp.openai_api:app --host 0.0.0.0 --port 9000"
PrivateTmp=true
 
[Install]
WantedBy=multi-user.target

接下来就可以正常使用了,这个API做的非常好,可以兼容各种客户端

Cloudflare AI

Workers AI allows you to run machine learning models, on the Cloudflare network, from your own code – whether that be from Workers, Pages, or anywhere via the Cloudflare API.

OpenAI 兼容 API 改造

调用 CF AI 的最好方式还是 worker, 那么只需要考虑怎么让输入输出在worker里处理就可以了

虽然我会讲的很简单,但是主要会面临两个问题。

  1. 判断是否流传输,根据可选项来调整输出方式(流传输是固定格式,有些不一样,且很多客户端只支持流传输)
  2. CF 目前还不支持流传输,所以这不仅是一个 API 代理
  3. 让 worker 流式传输

1. 简单的方式进行鉴权

CF AI 是有使用限制的,需要使用一个 token 来避免别人薅你

const authorization = request.headers.get("Authorization");
if (authorization !== `Bearer ${token}`) return Response.json({"message": "No permission invoke API"});

2. 接收请求和调用 CF AI

这部分还是比较简单,好在入参是一样的,交给CF算就成了,而且 CF 算的非常快。

// messages - chat style input
let chat = {
    messages: content.messages
};

let response = await ai.run(`@cf/meta/${cloudflare_model}`, chat);

3. 普通结果返回(Json)

其实相当多的参数都是写死的,包括 usage 部分,对于大多数客户端都没用,而且 CF 的计算公式不一样,所以不太能用得到。

let task =
    {
        "id": "cloudflare-ai",
        "object": "chat.completion",
        "created": new Date().getTime() / 1000,
        "model": cloudflare_model,
        "system_fingerprint": requestId,
        "choices": [
            {
                "index": 0,
                "message": {
                    "role": "assistant",
                    "content": content,
                },
                "finish_reason": "stop"
            }
        ],
        "usage": {
            "prompt_tokens": 10,
            "completion_tokens": 20,
            "total_tokens": 10
        }
    }

return Response.json(task);

4. 流响应

流响应是这个过程中比较难搞的部分,第一是因为大多客户端只有这种方式才能正常工作,例如 CodeGPT,第二是,worker 的 API 并没有详细说明这部分,所以这部分需要你了解有关 HTTP 流传输的相关知识。

可以留意下 下文的参考部分,非常有用

/**
 * write data to writer stream
 * @param {WritableStream} writable
 * @param {Array} msgBodyStream
 */
async function writeToStream(writable, msgBodyStream) {
    const writer = writable.getWriter();

    for (let msg of msgBodyStream) {
        setTimeout(async () => {
            const encoder = new TextEncoder();
            // 这是 OpenAI API 固定的格式,只能这么写,别的就不对
            await writer.write(encoder.encode(`data: ${JSON.stringify(msg)}` + '\n\n'));
        }, 200)
    }
}

const streamRender = (content) => {
    const {readable, writable} = new TransformStream()

    // 因为 CF 响应的太快了,而且不知道怎么拆分句子,所以,stream 简单的粗暴的分为3部分
    let stream = [
        // 固定开头
        {"object": "chat.completion.chunk", "choices": [{"delta": {"role": "assistant"}}]},
        // CF AI 响应的内容
        {"object": "chat.completion.chunk", "choices": [{"delta": {"content": content}}]},
        // 固定停止 finish_reason:stop
        {"object": "chat.completion.chunk", "choices": [{"delta": {}, "finish_reason": "stop"}]}
    ]

    let headers = new Headers();
    headers.append('Content-Type', 'text/event-stream');
    headers.append('Cache-Control', 'no-cache');

    const init = {"status": 200, "statusText": "ok", "headers": headers};

    writeToStream(writable, stream);

    return new Response(readable, init);
}

参考: