Skip to content

Commit 1436336

Browse files
committed
1.0.0
1 parent c77dd87 commit 1436336

18 files changed

+1056
-1
lines changed

.github/workflows/build.yml

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
name: 构建并发布Docker镜像
2+
3+
on:
4+
workflow_dispatch: # 允许手动触发工作流
5+
6+
jobs:
7+
build:
8+
runs-on: ubuntu-latest
9+
10+
steps:
11+
- name: 检出代码
12+
uses: actions/checkout@v3
13+
14+
- name: 设置Go环境
15+
uses: actions/setup-go@v4
16+
with:
17+
go-version: '1.23'
18+
19+
- name: 安装UPX
20+
run: sudo apt-get update && sudo apt-get install -y upx
21+
22+
- name: 构建Go程序
23+
run: go build -o aiload -ldflags -w main.go
24+
25+
- name: 使用UPX压缩可执行文件
26+
run: upx -9 aiload
27+
28+
- name: 登录到Docker Hub
29+
uses: docker/login-action@v3
30+
with:
31+
username: ${{ secrets.DOCKERHUB_USERNAME }}
32+
password: ${{ secrets.DOCKERHUB_TOKEN }}
33+
34+
- name: 设置Docker Buildx
35+
uses: docker/setup-buildx-action@v3
36+
37+
- name: 构建并推送Docker镜像
38+
uses: docker/build-push-action@v5
39+
with:
40+
context: .
41+
push: true
42+
tags: ${{ secrets.DOCKERHUB_USERNAME }}/aiload:latest

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
data/*

Dockerfile

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
FROM alpine:3.21
2+
RUN mkdir -p /opt/aiload/data
3+
WORKDIR /opt/aiload
4+
COPY aiload config.json /opt/aiload/
5+
# 暴露文件夹和端口
6+
EXPOSE 2081
7+
# 启动程序
8+
CMD ["./aiload", "start"]

README.md

+22-1
Original file line numberDiff line numberDiff line change
@@ -1 +1,22 @@
1-
# ai-relay
1+
# AILoad
2+
3+
AILoad 是一个轻量级且高效的工具,通过负载均衡的方式将请求分发到多个 AI 模型接口,从而有效解决单一 AI 模型频率上限问题。目前支持轮询和基于 IP 的负载均衡策略,能够在短时间内确保同一 IP 的请求被分配到同一个 AI 模型,提升一致性和用户体验。
4+
5+
## 主要功能
6+
7+
* 负载均衡 :自动将请求随机分发到多个 AI 模型接口,避免单个模型频率上限,提升整体吞吐量。
8+
* 统一API入口 :提供单一入口调用
9+
* 兼容OpenAI:完全兼容OpenAI API接口格式
10+
* 流式返回:支持流式传输
11+
* IP 亲和性:同一个IP在一定时间内固定分配到一个AI模型,提升连续对话一致性
12+
13+
14+
## 使用场景
15+
16+
目前市面部分厂商提供了免费的AI模型接口,但往往存在频率限制,通过AILoad可以将多个AI接口整合到一起,并通过负载均衡的方式来缓解单一AI模型频率上限的问题。
17+
18+
## 部署
19+
20+
## 联系我们
21+
22+
如有任何问题或反馈,请通过 GitHub Issues提交。

api/home.go

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package api
2+
3+
import (
4+
"github.com/gin-gonic/gin"
5+
)
6+
7+
func Home(c *gin.Context) {
8+
// 准备 HTML 内容
9+
htmlContent := `<p>Please visit <a href="https://github.com/helloxz/aiload" target="_blank">helloxz/aiload</a> for usage instructions.</p>`
10+
11+
// 设置 Content-Type 为 text/html
12+
c.Header("Content-Type", "text/html; charset=utf-8")
13+
14+
// 返回 HTML 内容
15+
c.String(200, htmlContent)
16+
}

api/relay.go

+160
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
package api
2+
3+
import (
4+
"aiload/utils"
5+
"math/rand"
6+
"os"
7+
"strconv"
8+
"time"
9+
10+
"github.com/gin-gonic/gin"
11+
"github.com/spf13/viper"
12+
"github.com/tidwall/gjson"
13+
)
14+
15+
// 声明一个结构体
16+
type Model struct {
17+
BaseURL string `json:"base_url"`
18+
Model string `json:"model"`
19+
ApiKey string `json:"api_key"`
20+
}
21+
22+
func Relay(c *gin.Context) {
23+
// 读取 JSON 文件内容
24+
data, err := os.ReadFile("data/config/config.json")
25+
if err != nil {
26+
c.JSON(200, gin.H{
27+
"code": 500,
28+
"msg": "Failed to read configuration file!",
29+
"data": "",
30+
})
31+
c.Abort()
32+
return
33+
}
34+
35+
// 获取原始请求体数据
36+
rawData, err := c.GetRawData()
37+
if err != nil {
38+
c.JSON(200, gin.H{
39+
"code": 500,
40+
"msg": "Failed to read request body!",
41+
"data": "",
42+
})
43+
c.Abort()
44+
return
45+
}
46+
47+
stream := gjson.GetBytes(rawData, "stream").Bool()
48+
49+
// 将文件内容转换为字符串
50+
jsonData := string(data)
51+
52+
// 重要:将请求体放回,这样后续处理器仍然可以读取它
53+
c.Request.Body = utils.CreateReadCloser(rawData)
54+
55+
// 使用 gjson 提取 models 列表
56+
models := gjson.Get(jsonData, "models")
57+
if !models.Exists() {
58+
c.JSON(200, gin.H{
59+
"code": 500,
60+
"msg": "Models list not found!",
61+
"data": "",
62+
})
63+
c.Abort()
64+
return
65+
}
66+
67+
var modelList []Model
68+
69+
// 遍历 models 列表
70+
models.ForEach(func(_, model gjson.Result) bool {
71+
baseURL := model.Get("base_url").String()
72+
modelName := model.Get("model").String()
73+
api_key := model.Get("api_key").String()
74+
75+
// 将每个模型的信息添加到 modelList 中
76+
modelList = append(modelList, Model{
77+
BaseURL: baseURL,
78+
Model: modelName,
79+
ApiKey: api_key,
80+
})
81+
return true // 继续遍历下一个元素
82+
})
83+
84+
// 随机从 modelList 中选择一个模型
85+
randIndex := rand.Intn(len(modelList))
86+
// 检查内存中是否存在index
87+
index := getModelIndex(c)
88+
// 如果不存在
89+
if index == 0 {
90+
// 将选择的模型的 index 存入缓存
91+
setModelIndex(c, randIndex)
92+
} else {
93+
randIndex = index
94+
}
95+
96+
selectedModel := modelList[randIndex]
97+
98+
// fmt.Println(selectedModel)
99+
timeout := viper.GetInt("settins.timeout")
100+
// 如果为0,则设置默认值
101+
if timeout == 0 {
102+
timeout = 120
103+
}
104+
config := utils.ProxyConfig{
105+
TargetURL: selectedModel.BaseURL,
106+
Timeout: time.Duration(timeout) * time.Second, // 设置超时时间
107+
CustomHeader: map[string]string{"Authorization": "Bearer " + selectedModel.ApiKey},
108+
RemoveHeader: []string{},
109+
AddHeader: map[string]string{"X-Model": selectedModel.Model}, // 添加自定义响应头
110+
ModelName: selectedModel.Model,
111+
}
112+
113+
if stream {
114+
// 请求转发服务
115+
utils.ReverseStreamProxy(c, config)
116+
return
117+
} else {
118+
// 请求转发服务
119+
utils.ReverseProxy(c, config)
120+
}
121+
122+
}
123+
124+
// 获取模型的index
125+
func getModelIndex(c *gin.Context) int {
126+
// 获取用户IP
127+
ip := utils.GetClientIP(c)
128+
// 生成key
129+
keyStr := "model_index_" + ip
130+
// 转为[]byte
131+
key := []byte(keyStr)
132+
// 读取缓存
133+
value, err := utils.Cache.Get(key)
134+
if err != nil {
135+
return 0
136+
}
137+
if value == nil {
138+
return 0
139+
}
140+
// 转为int
141+
index, err := strconv.Atoi(string(value))
142+
if err != nil {
143+
return 0
144+
}
145+
return index
146+
}
147+
148+
// 设置模型的index
149+
func setModelIndex(c *gin.Context, index int) {
150+
// 获取用户IP
151+
ip := utils.GetClientIP(c)
152+
// 生成key
153+
keyStr := "model_index_" + ip
154+
// 转为[]byte
155+
key := []byte(keyStr)
156+
// 转为[]byte
157+
value := []byte(strconv.Itoa(index))
158+
// 设置缓存,有效期5分钟
159+
utils.Cache.Set(key, value, 60*5)
160+
}

config.json

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"auth_token": "",
3+
"models": [
4+
{
5+
"api_key": "sk-xxx",
6+
"base_url": "https://api.openai.com/v1/chat/completions",
7+
"model": "gpt-4o"
8+
}
9+
],
10+
"server": {
11+
"model": "release",
12+
"port": 2081
13+
},
14+
"settings":{
15+
"timeout":120
16+
}
17+
}

go.mod

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
module aiload
2+
3+
go 1.23.2
4+
5+
require (
6+
github.com/gin-gonic/gin v1.10.0
7+
github.com/spf13/viper v1.19.0
8+
)
9+
10+
require (
11+
github.com/bytedance/sonic v1.11.6 // indirect
12+
github.com/bytedance/sonic/loader v0.1.1 // indirect
13+
github.com/cespare/xxhash/v2 v2.3.0 // indirect
14+
github.com/cloudwego/base64x v0.1.4 // indirect
15+
github.com/cloudwego/iasm v0.2.0 // indirect
16+
github.com/coocood/freecache v1.2.4 // indirect
17+
github.com/fsnotify/fsnotify v1.7.0 // indirect
18+
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
19+
github.com/gin-contrib/sse v0.1.0 // indirect
20+
github.com/go-playground/locales v0.14.1 // indirect
21+
github.com/go-playground/universal-translator v0.18.1 // indirect
22+
github.com/go-playground/validator/v10 v10.20.0 // indirect
23+
github.com/go-resty/resty/v2 v2.16.5 // indirect
24+
github.com/goccy/go-json v0.10.2 // indirect
25+
github.com/hashicorp/hcl v1.0.0 // indirect
26+
github.com/json-iterator/go v1.1.12 // indirect
27+
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
28+
github.com/leodido/go-urn v1.4.0 // indirect
29+
github.com/magiconair/properties v1.8.7 // indirect
30+
github.com/mattn/go-isatty v0.0.20 // indirect
31+
github.com/mitchellh/mapstructure v1.5.0 // indirect
32+
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
33+
github.com/modern-go/reflect2 v1.0.2 // indirect
34+
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
35+
github.com/sagikazarmark/locafero v0.4.0 // indirect
36+
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
37+
github.com/sourcegraph/conc v0.3.0 // indirect
38+
github.com/spf13/afero v1.11.0 // indirect
39+
github.com/spf13/cast v1.6.0 // indirect
40+
github.com/spf13/pflag v1.0.5 // indirect
41+
github.com/subosito/gotenv v1.6.0 // indirect
42+
github.com/tidwall/gjson v1.18.0 // indirect
43+
github.com/tidwall/match v1.1.1 // indirect
44+
github.com/tidwall/pretty v1.2.1 // indirect
45+
github.com/tidwall/sjson v1.2.5 // indirect
46+
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
47+
github.com/ugorji/go/codec v1.2.12 // indirect
48+
go.uber.org/atomic v1.9.0 // indirect
49+
go.uber.org/multierr v1.9.0 // indirect
50+
golang.org/x/arch v0.8.0 // indirect
51+
golang.org/x/crypto v0.31.0 // indirect
52+
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
53+
golang.org/x/net v0.33.0 // indirect
54+
golang.org/x/sys v0.28.0 // indirect
55+
golang.org/x/text v0.21.0 // indirect
56+
google.golang.org/protobuf v1.34.1 // indirect
57+
gopkg.in/ini.v1 v1.67.0 // indirect
58+
gopkg.in/yaml.v3 v3.0.1 // indirect
59+
)

0 commit comments

Comments
 (0)