From 7ffb0e2e195901c0991964273aee3b5bdd2356b7 Mon Sep 17 00:00:00 2001 From: TGY Date: Thu, 20 Mar 2025 17:01:15 +0800 Subject: [PATCH] add uni-api --- uni-api/README.md | 507 ++++++++++++++++++++++++++++++ uni-api/data.yml | 19 ++ uni-api/latest/data.yml | 35 +++ uni-api/latest/docker-compose.yml | 21 ++ uni-api/logo.png | Bin 0 -> 43853 bytes 5 files changed, 582 insertions(+) create mode 100644 uni-api/README.md create mode 100644 uni-api/data.yml create mode 100644 uni-api/latest/data.yml create mode 100644 uni-api/latest/docker-compose.yml create mode 100644 uni-api/logo.png diff --git a/uni-api/README.md b/uni-api/README.md new file mode 100644 index 000000000..f795b5636 --- /dev/null +++ b/uni-api/README.md @@ -0,0 +1,507 @@ +# uni-api + +

+ + + + + docker pull + +

+ +[英文](./README.md) | [中文](./README_CN.md) + +## 介绍 + +如果个人使用的话,one/new-api 过于复杂,有很多个人不需要使用的商用功能,如果你不想要复杂的前端界面,又想要支持的模型多一点,可以试试 uni-api。这是一个统一管理大模型 API 的项目,可以通过一个统一的API 接口调用多种不同提供商的服务,统一转换为 OpenAI 格式,支持负载均衡。目前支持的后端服务有:OpenAI、Anthropic、Gemini、Vertex、Azure、xai、Cohere、Groq、Cloudflare、OpenRouter 等。 + +## ✨ 特性 + +- 无前端,纯配置文件配置 API 渠道。只要写一个文件就能运行起一个属于自己的 API 站,文档有详细的配置指南,小白友好。 +- 统一管理多个后端服务,支持 OpenAI、Deepseek、OpenRouter 等其他 API 是 OpenAI 格式的提供商。支持 OpenAI Dalle-3 图像生成。 +- 同时支持 Anthropic、Gemini、Vertex AI、Azure、xai、Cohere、Groq、Cloudflare。Vertex 同时支持 Claude 和 Gemini API。 +- 支持 OpenAI、 Anthropic、Gemini、Vertex、Azure、xai 原生 tool use 函数调用。 +- 支持 OpenAI、Anthropic、Gemini、Vertex、Azure、xai 原生识图 API。 +- 支持四种负载均衡。 + 1. 支持渠道级加权负载均衡,可以根据不同的渠道权重分配请求。默认不开启,需要配置渠道权重。 + 2. 支持 Vertex 区域级负载均衡,支持 Vertex 高并发,最高可将 Gemini,Claude 并发提高 (API数量 * 区域数量) 倍。自动开启不需要额外配置。 + 3. 除了 Vertex 区域级负载均衡,所有 API 均支持渠道级顺序负载均衡,提高沉浸式翻译体验。默认不开启,需要配置 `SCHEDULING_ALGORITHM` 为 `round_robin`。 + 4. 支持单个渠道多个 API Key 自动开启 API key 级别的轮训负载均衡。 +- 支持自动重试,当一个 API 渠道响应失败时,自动重试下一个 API 渠道。 +- 支持渠道冷却,当一个 API 渠道响应失败时,会自动将该渠道排除冷却一段时间,不再请求该渠道,冷却时间结束后,会自动将该模型恢复,直到再次请求失败,会重新冷却。 +- 支持细粒度的模型超时时间设置,可以为每个模型设置不同的超时时间。 +- 支持细粒度的权限控制。支持使用通配符设置 API key 可用渠道的特定模型。 +- 支持限流,可以设置每分钟最多请求次数,可以设置为整数,如 2/min,2 次每分钟、5/hour,5 次每小时、10/day,10 次每天,10/month,10 次每月,10/year,10 次每年。默认60/min。 +- 支持多个标准 OpenAI 格式的接口:`/v1/chat/completions`,`/v1/images/generations`,`/v1/audio/transcriptions`,`/v1/moderations`,`/v1/models`。 +- 支持 OpenAI moderation 道德审查,可以对用户的消息进行道德审查,如果发现不当的消息,会返回错误信息。降低后台 API 被提供商封禁的风险。 + +## 使用方法 + +启动 uni-api 必须使用配置文件,有两种方式可以启动配置文件: + +1. 第一种是使用 `CONFIG_URL` 环境变量填写配置文件 URL,uni-api启动时会自动下载。 +2. 第二种就是挂载名为 `api.yaml` 的配置文件到容器内。 + +### 方法一:挂载 `api.yaml` 配置文件启动 uni-api + +必须事先填写完成配置文件才能启动 `uni-api`,必须使用名为 `api.yaml` 的配置文件才能启动 `uni-api`,可以配置多个模型,每个模型可以配置多个后端服务,支持负载均衡。下面是最小可运行的 `api.yaml` 配置文件的示例: + +```yaml +providers: + - provider: provider_name # 服务提供商名称, 如 openai、anthropic、gemini、openrouter,随便取名字,必填 + base_url: https://api.your.com/v1/chat/completions # 后端服务的API地址,必填 + api: sk-YgS6GTi0b4bEabc4C # 提供商的API Key,必填,自动使用 base_url 和 api 通过 /v1/models 端点获取可用的所有模型。 + # 这里可以配置多个提供商,每个提供商可以配置多个 API Key,每个提供商可以配置多个模型。 +api_keys: + - api: sk-Pkj60Yf8JFWxfgRmXQFWyGtWUddGZnmi3KlvowmRWpWpQxx # API Key,用户请求 uni-api 需要 API key,必填 + # 该 API Key 可以使用所有模型,即可以使用 providers 下面设置的所有渠道里面的所有模型,不需要一个个添加可用渠道。 +``` + +`api.yaml` 详细的高级配置: + +```yaml +providers: + - provider: provider_name # 服务提供商名称, 如 openai、anthropic、gemini、openrouter,随便取名字,必填 + base_url: https://api.your.com/v1/chat/completions # 后端服务的API地址,必填 + api: sk-YgS6GTi0b4bEabc4C # 提供商的API Key,必填 + model: # 选填,如果不配置 model,会自动通过 base_url 和 api 通过 /v1/models 端点获取可用的所有模型。 + - gpt-4o # 可以使用的模型名称,必填 + - claude-3-5-sonnet-20240620: claude-3-5-sonnet # 重命名模型,claude-3-5-sonnet-20240620 是服务商的模型名称,claude-3-5-sonnet 是重命名后的名字,可以使用简洁的名字代替原来复杂的名称,选填 + - dall-e-3 + + - provider: anthropic + base_url: https://api.anthropic.com/v1/messages + api: # 支持多个 API Key,多个 key 自动开启轮训负载均衡,至少一个 key,必填 + - sk-ant-api03-bNnAOJyA-xQw_twAA + - sk-ant-api02-bNnxxxx + model: + - claude-3-7-sonnet-20240620: claude-3-7-sonnet # 重命名模型,claude-3-7-sonnet-20240620 是服务商的模型名称,claude-3-7-sonnet 是重命名后的名字,可以使用简洁的名字代替原来复杂的名称,选填 + - claude-3-7-sonnet-20250219: claude-3-7-sonnet-think # 重命名模型,claude-3-7-sonnet-20250219 是服务商的模型名称,claude-3-7-sonnet-think 是重命名后的名字,可以使用简洁的名字代替原来复杂的名称,如果重命名后的名字里面有think,则自动转换为 claude 思考模型,默认思考 token 限制为 4096。选填 + tools: true # 是否支持工具,如生成代码、生成文档等,默认是 true,选填 + + - provider: gemini + base_url: https://generativelanguage.googleapis.com/v1beta # base_url 支持 v1beta/v1, 仅供 Gemini 模型使用,必填 + api: # 支持多个 API Key,多个 key 自动开启轮训负载均衡,至少一个 key,必填 + - AIzaSyAN2k6IRdgw123 + - AIzaSyAN2k6IRdgw456 + - AIzaSyAN2k6IRdgw789 + model: + - gemini-1.5-pro + - gemini-1.5-flash-exp-0827: gemini-1.5-flash # 重命名后,原来的模型名字 gemini-1.5-flash-exp-0827 无法使用,如果要使用原来的名字,可以在 model 中添加原来的名字,只要加上下面一行就可以使用原来的名字了 + - gemini-1.5-flash-exp-0827 # 加上这一行,gemini-1.5-flash-exp-0827 和 gemini-1.5-flash 都可以被请求 + - gemini-1.5-pro: gemini-1.5-pro-search # 支持以 -search 后缀重命名模型启用搜索,使用 gemini-1.5-pro-search 模型请求 uni-api 时,表示 gemini-1.5-pro 模型自动使用 Google 官方搜索工具,支持全部 1.5/2.0 系列模型。 + tools: true + preferences: + api_key_rate_limit: 15/min # 每个 API Key 每分钟最多请求次数,选填。默认为 999999/min。支持多个频率约束条件:15/min,10/day + # api_key_rate_limit: # 可以为每个模型设置不同的频率限制 + # gemini-1.5-flash: 15/min,1500/day + # gemini-1.5-pro: 2/min,50/day + # default: 4/min # 如果模型没有设置频率限制,使用 default 的频率限制 + api_key_cooldown_period: 60 # 每个 API Key 遭遇 429 错误后的冷却时间,单位为秒,选填。默认为 0 秒, 当设置为 0 秒时,不启用冷却机制。当存在多个 API key 时才会生效。 + api_key_schedule_algorithm: round_robin # 设置多个 API Key 的请求顺序,选填。默认为 round_robin,可选值有:round_robin,random,fixed_priority。当存在多个 API key 时才会生效。round_robin 是轮询负载均衡,random 是随机负载均衡,fixed_priority 是固定优先级调度,永远使用第一个可用的 API key。 + model_timeout: # 模型超时时间,单位为秒,默认 100 秒,选填 + gemini-1.5-pro: 10 # 模型 gemini-1.5-pro 的超时时间为 10 秒 + gemini-1.5-flash: 10 # 模型 gemini-1.5-flash 的超时时间为 10 秒 + default: 10 # 模型没有设置超时时间,使用默认的超时时间 10 秒,当请求的不在 model_timeout 里面的模型时,超时时间默认是 10 秒,不设置 default,uni-api 会使用全局配置的模型超时时间。 + proxy: socks5://[用户名]:[密码]@[IP地址]:[端口] # 代理地址,选填。支持 socks5 和 http 代理,默认不使用代理。 + headers: # 额外附加自定义HTTP请求头,选填。 + Custom-Header-1: Value-1 + Custom-Header-2: Value-2 + + - provider: vertex + project_id: gen-lang-client-xxxxxxxxxxxxxx # 描述: 您的Google Cloud项目ID。格式: 字符串,通常由小写字母、数字和连字符组成。获取方式: 在Google Cloud Console的项目选择器中可以找到您的项目ID。 + private_key: "-----BEGIN PRIVATE KEY-----\nxxxxx\n-----END PRIVATE" # 描述: Google Cloud Vertex AI服务账号的私钥。格式: 一个 JSON 格式的字符串,包含服务账号的私钥信息。获取方式: 在 Google Cloud Console 中创建服务账号,生成JSON格式的密钥文件,然后将其内容设置为此环境变量的值。 + client_email: xxxxxxxxxx@xxxxxxx.gserviceaccount.com # 描述: Google Cloud Vertex AI 服务账号的电子邮件地址。格式: 通常是形如 "service-account-name@project-id.iam.gserviceaccount.com" 的字符串。获取方式: 在创建服务账号时生成,也可以在 Google Cloud Console 的"IAM与管理"部分查看服务账号详情获得。 + model: + - gemini-1.5-pro + - gemini-1.5-flash + - gemini-1.5-pro: gemini-1.5-pro-search # 仅支持在 vertex Gemini API 中,以 -search 后缀重命名模型后,使用 gemini-1.5-pro-search 模型请求 uni-api 时,表示 gemini-1.5-pro 模型自动使用 Google 官方搜索工具。 + - claude-3-5-sonnet@20240620: claude-3-5-sonnet + - claude-3-opus@20240229: claude-3-opus + - claude-3-sonnet@20240229: claude-3-sonnet + - claude-3-haiku@20240307: claude-3-haiku + tools: true + notes: https://xxxxx.com/ # 可以放服务商的网址,备注信息,官方文档,选填 + + - provider: cloudflare + api: f42b3xxxxxxxxxxq4aoGAh # Cloudflare API Key,必填 + cf_account_id: 8ec0xxxxxxxxxxxxe721 # Cloudflare Account ID,必填 + model: + - '@cf/meta/llama-3.1-8b-instruct': llama-3.1-8b # 重命名模型,@cf/meta/llama-3.1-8b-instruct 是服务商的原始的模型名称,必须使用引号包裹模型名,否则yaml语法错误,llama-3.1-8b 是重命名后的名字,可以使用简洁的名字代替原来复杂的名称,选填 + - '@cf/meta/llama-3.1-8b-instruct' # 必须使用引号包裹模型名,否则yaml语法错误 + + - provider: azure + base_url: https://your-endpoint.openai.azure.com + api: your-api-key + model: + - gpt-4o + + - provider: other-provider + base_url: https://api.xxx.com/v1/messages + api: sk-bNnAOJyA-xQw_twAA + model: + - causallm-35b-beta2ep-q6k: causallm-35b + - anthropic/claude-3-5-sonnet + tools: false + engine: openrouter # 强制使用某个消息格式,目前支持 gpt,claude,gemini,openrouter 原生格式,选填 + +api_keys: + - api: sk-KjjI60Yf0JFWxfgRmXqFWyGtWUd9GZnmi3KlvowmRWpWpQRo # API Key,用户使用本服务需要 API key,必填 + model: # 该 API Key 可以使用的模型,必填。默认开启渠道级轮询负载均衡,每次请求模型按照 model 配置的顺序依次请求。与 providers 里面原始的渠道顺序无关。因此你可以设置每个 API key 请求顺序不一样。 + - gpt-4o # 可以使用的模型名称,可以使用所有提供商提供的 gpt-4o 模型 + - claude-3-5-sonnet # 可以使用的模型名称,可以使用所有提供商提供的 claude-3-5-sonnet 模型 + - gemini/* # 可以使用的模型名称,仅可以使用名为 gemini 提供商提供的所有模型,其中 gemini 是 provider 名称,* 代表所有模型 + role: admin # 设置 API key 的别名,选填。请求日志会显示该 API key 的别名。如果 role 为 admin,则仅有此 API key 可以请求 v1/stats,/v1/generate-api-key 端点。如果所有 API key 都没有设置 role 为 admin,则默认第一个 API key 为 admin 拥有请求 v1/stats,/v1/generate-api-key 端点的权限。 + + - api: sk-pkhf60Yf0JGyJxgRmXqFQyTgWUd9GZnmi3KlvowmRWpWqrhy + model: + - anthropic/claude-3-5-sonnet # 可以使用的模型名称,仅可以使用名为 anthropic 提供商提供的 claude-3-5-sonnet 模型。其他提供商的 claude-3-5-sonnet 模型不可以使用。这种写法不会匹配到other-provider提供的名为anthropic/claude-3-5-sonnet的模型。 + - # 通过在模型名两侧加上尖括号,这样就不会去名为anthropic的渠道下去寻找claude-3-5-sonnet模型,而是将整个 anthropic/claude-3-5-sonnet 作为模型名称。这种写法可以匹配到other-provider提供的名为 anthropic/claude-3-5-sonnet 的模型。但不会匹配到anthropic下面的claude-3-5-sonnet模型。 + - openai-test/text-moderation-latest # 当开启消息道德审查后,可以使用名为 openai-test 渠道下的 text-moderation-latest 模型进行道德审查。 + - sk-KjjI60Yd0JFWtxxxxxxxxxxxxxxwmRWpWpQRo/* # 支持将其他 api key 当作渠道 + preferences: + SCHEDULING_ALGORITHM: fixed_priority # 当 SCHEDULING_ALGORITHM 为 fixed_priority 时,使用固定优先级调度,永远执行第一个拥有请求的模型的渠道。默认开启,SCHEDULING_ALGORITHM 缺省值为 fixed_priority。SCHEDULING_ALGORITHM 可选值有:fixed_priority,round_robin,weighted_round_robin, lottery, random。 + # 当 SCHEDULING_ALGORITHM 为 random 时,使用随机轮训负载均衡,随机请求拥有请求的模型的渠道。 + # 当 SCHEDULING_ALGORITHM 为 round_robin 时,使用轮训负载均衡,按照顺序请求用户使用的模型的渠道。 + AUTO_RETRY: true # 是否自动重试,自动重试下一个提供商,true 为自动重试,false 为不自动重试,默认为 true。也可以设置为数字,表示重试次数。 + rate_limit: 15/min # 支持限流,每分钟最多请求次数,可以设置为整数,如 2/min,2 次每分钟、5/hour,5 次每小时、10/day,10 次每天,10/month,10 次每月,10/year,10 次每年。默认999999/min,选填。支持多个频率约束条件:15/min,10/day + # rate_limit: # 可以为每个模型设置不同的频率限制 + # gemini-1.5-flash: 15/min,1500/day + # gemini-1.5-pro: 2/min,50/day + # default: 4/min # 如果模型没有设置频率限制,使用 default 的频率限制 + ENABLE_MODERATION: true # 是否开启消息道德审查,true 为开启,false 为不开启,默认为 false,当开启后,会对用户的消息进行道德审查,如果发现不当的消息,会返回错误信息。 + + # 渠道级加权负载均衡配置示例 + - api: sk-KjjI60Yd0JFWtxxxxxxxxxxxxxxwmRWpWpQRo + model: + - gcp1/*: 5 # 冒号后面就是权重,权重仅支持正整数。 + - gcp2/*: 3 # 数字的大小代表权重,数字越大,请求的概率越大。 + - gcp3/*: 2 # 在该示例中,所有渠道加起来一共有 10 个权重,及 10 个请求里面有 5 个请求会请求 gcp1/* 模型,2 个请求会请求 gcp2/* 模型,3 个请求会请求 gcp3/* 模型。 + + preferences: + SCHEDULING_ALGORITHM: weighted_round_robin # 仅当 SCHEDULING_ALGORITHM 为 weighted_round_robin 并且上面的渠道如果有权重,会按照加权后的顺序请求。使用加权轮训负载均衡,按照权重顺序请求拥有请求的模型的渠道。当 SCHEDULING_ALGORITHM 为 lottery 时,使用抽奖轮训负载均衡,按照权重随机请求拥有请求的模型的渠道。没设置权重的渠道自动回退到 round_robin 轮训负载均衡。 + AUTO_RETRY: true + +preferences: # 全局配置 + model_timeout: # 模型超时时间,单位为秒,默认 100 秒,选填 + gpt-4o: 10 # 模型 gpt-4o 的超时时间为 10 秒,gpt-4o 是模型名称,当请求 gpt-4o-2024-08-06 等模型时,超时时间也是 10 秒 + claude-3-5-sonnet: 10 # 模型 claude-3-5-sonnet 的超时时间为 10 秒,当请求 claude-3-5-sonnet-20240620 等模型时,超时时间也是 10 秒 + default: 10 # 模型没有设置超时时间,使用默认的超时时间 10 秒,当请求的不在 model_timeout 里面的模型时,超时时间默认是 10 秒,不设置 default,uni-api 会使用 环境变量 TIMEOUT 设置的默认超时时间,默认超时时间是 100 秒 + o1-mini: 30 # 模型 o1-mini 的超时时间为 30 秒,当请求名字是 o1-mini 开头的模型时,超时时间是 30 秒 + o1-preview: 100 # 模型 o1-preview 的超时时间为 100 秒,当请求名字是 o1-preview 开头的模型时,超时时间是 100 秒 + cooldown_period: 300 # 渠道冷却时间,单位为秒,默认 300 秒,选填。当模型请求失败时,会自动将该渠道排除冷却一段时间,不再请求该渠道,冷却时间结束后,会自动将该模型恢复,直到再次请求失败,会重新冷却。当 cooldown_period 设置为 0 时,不启用冷却机制。 + rate_limit: 999999/min # uni-api 全局速率限制,单位为次数/分钟,支持多个频率约束条件,例如:15/min,10/day。默认 999999/min,选填。 + error_triggers: # 错误触发器,当模型返回的消息包含错误触发器中的任意一个字符串时,该渠道会自动返回报错。选填 + - The bot's usage is covered by the developer + - process this request due to overload or policy + proxy: socks5://[username]:[password]@[ip]:[port] # 全局代理地址,选填。 +``` + +挂载配置文件并启动 uni-api docker 容器: + +```bash +docker run --user root -p 8001:8000 --name uni-api -dit \ +-v ./api.yaml:/home/api.yaml \ +yym68686/uni-api:latest +``` + +### 方法二:使用 `CONFIG_URL` 环境变量启动 uni-api + +按照方法一写完配置文件后,上传到云端硬盘,获取文件的直链,然后使用 `CONFIG_URL` 环境变量启动 uni-api docker 容器: + +```bash +docker run --user root -p 8001:8000 --name uni-api -dit \ +-e CONFIG_URL=http://file_url/api.yaml \ +yym68686/uni-api:latest +``` + +## 环境变量 + +- CONFIG_URL: 配置文件的下载地址,可以是本地文件,也可以是远程文件,选填 +- TIMEOUT: 请求超时时间,默认为 100 秒,超时时间可以控制当一个渠道没有响应时,切换下一个渠道需要的时间。选填 +- DISABLE_DATABASE: 是否禁用数据库,默认为 false,选填 + +## Vercel 部署 + +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fyym68686%2Funi-api%2Ftree%2Fmain&env=CONFIG_URL,DISABLE_DATABASE&project-name=uni-api-vercel&repository-name=uni-api-vercel) + +点击上面的一键部署按钮后,设置环境变量 `CONFIG_URL` 为配置文件的直链, `DISABLE_DATABASE` 为 true,然后点击 Create 创建项目。部署完之后需要手动在 vercel 项目面板的 Settings -> Funcitons -> Function Max Duration 设置为 60 秒,然后点击 Deployments 菜单点击 Redeploy 重新部署,即可将超时时间设置为 60 秒,如果不重新部署,默认超时时间将是原来的 10 秒。注意不是删掉 vercel 项目重建,而是在当前部署好的 vercel 项目里面的 Deployments 菜单里面点 redeploy,这样才能让 Function Max Duration 的修改生效。 + +## Ubuntu 部署 + +在仓库 Releases 找到对应的二进制文件最新版本,例如名为 uni-api-linux-x86_64-0.0.99.pex 的文件。在服务器下载二进制文件并运行: + +```bash +wget https://github.com/yym68686/uni-api/releases/download/v0.0.99/uni-api-linux-x86_64-0.0.99.pex +chmod +x uni-api-linux-x86_64-0.0.99.pex +./uni-api-linux-x86_64-0.0.99.pex +``` + +## serv00 远程部署(FreeBSD 14.0) + +首先登录面板,Additional services 里面点击选项卡 Run your own applications 开启允许运行自己的程序,然后到面板 Port reservation 去随便开一个端口。 + +如果没有自己的域名,去面板 WWW websites 删掉默认给的域名,再新建一个域名 Domain 为刚才删掉的域名,点击 Advanced settings 后设置 Website type 为 Proxy 域名,Proxy port 指向你刚才开的端口,不要选中 Use HTTPS。 + +ssh 登陆到 serv00 服务器,执行下面的命令: + +```bash +git clone --depth 1 -b main --quiet https://github.com/yym68686/uni-api.git +cd uni-api +python -m venv uni-api +tmux new -A -s uni-api +source uni-api/bin/activate +export CFLAGS="-I/usr/local/include" +export CXXFLAGS="-I/usr/local/include" +export CC=gcc +export CXX=g++ +export MAX_CONCURRENCY=1 +export CPUCOUNT=1 +export MAKEFLAGS="-j1" +CMAKE_BUILD_PARALLEL_LEVEL=1 cpuset -l 0 pip install -vv -r requirements.txt +cpuset -l 0 pip install -r -vv requirements.txt +``` + +ctrl+b d 退出 tmux 等待几个小时安装完成,安装完成后执行下面的命令: + +```bash +tmux new -A -s uni-api +source uni-api/bin/activate +export CONFIG_URL=http://file_url/api.yaml +export DISABLE_DATABASE=true +# 修改端口,xxx 为端口,自行修改,对应刚刚在面板 Port reservation 开的端口 +sed -i '' 's/port=8000/port=xxx/' main.py +sed -i '' 's/reload=True/reload=False/' main.py +python main.py +``` + +使用 ctrl+b d 退出 tmux,即可让程序后台运行。此时就可以在其他聊天客户端使用 uni-api 了。curl 测试脚本: + +```bash +curl -X POST https://xxx.serv00.net/v1/chat/completions \ +-H 'Content-Type: application/json' \ +-H 'Authorization: Bearer sk-xxx' \ +-d '{"model": "gpt-4o","messages": [{"role": "user","content": "你好"}]}' +``` + +参考文档: + +https://docs.serv00.com/Python/ + +https://linux.do/t/topic/201181 + +https://linux.do/t/topic/218738 + +## Docker 本地部署 + +Start the container + +```bash +docker run --user root -p 8001:8000 --name uni-api -dit \ +-e CONFIG_URL=http://file_url/api.yaml \ # 如果已经挂载了本地配置文件,不需要设置 CONFIG_URL +-v ./api.yaml:/home/api.yaml \ # 如果已经设置 CONFIG_URL,不需要挂载配置文件 +-v ./uniapi_db:/home/data \ # 如果不想保存统计数据,不需要挂载该文件夹 +yym68686/uni-api:latest +``` + +Or if you want to use Docker Compose, here is a docker-compose.yml example: + +```yaml +services: + uni-api: + container_name: uni-api + image: yym68686/uni-api:latest + environment: + - CONFIG_URL=http://file_url/api.yaml # 如果已经挂载了本地配置文件,不需要设置 CONFIG_URL + ports: + - 8001:8000 + volumes: + - ./api.yaml:/home/api.yaml # 如果已经设置 CONFIG_URL,不需要挂载配置文件 + - ./uniapi_db:/home/data # 如果不想保存统计数据,不需要挂载该文件夹 +``` + +CONFIG_URL 就是可以自动下载远程的配置文件。比如你在某个平台不方便修改配置文件,可以把配置文件传到某个托管服务,可以提供直链给 uni-api 下载,CONFIG_URL 就是这个直链。如果使用本地挂载的配置文件,不需要设置 CONFIG_URL。CONFIG_URL 是在不方便挂载配置文件的情况下使用。 + +Run Docker Compose container in the background + +```bash +docker-compose pull +docker-compose up -d +``` + +Docker build + +```bash +docker build --no-cache -t uni-api:latest -f Dockerfile --platform linux/amd64 . +docker tag uni-api:latest yym68686/uni-api:latest +docker push yym68686/uni-api:latest +``` + +One-Click Restart Docker Image + +```bash +set -eu +docker pull yym68686/uni-api:latest +docker rm -f uni-api +docker run --user root -p 8001:8000 -dit --name uni-api \ +-e CONFIG_URL=http://file_url/api.yaml \ +-v ./api.yaml:/home/api.yaml \ +-v ./uniapi_db:/home/data \ +yym68686/uni-api:latest +docker logs -f uni-api +``` + +RESTful curl test + +```bash +curl -X POST http://127.0.0.1:8000/v1/chat/completions \ +-H "Content-Type: application/json" \ +-H "Authorization: Bearer ${API}" \ +-d '{"model": "gpt-4o","messages": [{"role": "user", "content": "Hello"}],"stream": true}' +``` + +pex linux 打包: + +```bash +VERSION=$(cat VERSION) +pex -D . -r requirements.txt \ + -c uvicorn \ + --inject-args 'main:app --host 0.0.0.0 --port 8000' \ + --platform linux_x86_64-cp-3.10.12-cp310 \ + --interpreter-constraint '==3.10.*' \ + --no-strip-pex-env \ + -o uni-api-linux-x86_64-${VERSION}.pex +``` + +macos 打包: + +```bash +VERSION=$(cat VERSION) +pex -r requirements.txt \ + -c uvicorn \ + --inject-args 'main:app --host 0.0.0.0 --port 8000' \ + -o uni-api-macos-arm64-${VERSION}.pex +``` + +## 赞助商 + +我们感谢以下赞助商的支持: + +- @PowerHunter:¥2000 +- @IM4O4: ¥100 +- @ioi:¥50 + +## 如何赞助我们 + +如果您想支持我们的项目,您可以通过以下方式赞助我们: + +1. [PayPal](https://www.paypal.me/yym68686) + +2. [USDT-TRC20](https://pb.yym68686.top/~USDT-TRC20),USDT-TRC20 钱包地址:`TLFbqSv5pDu5he43mVmK1dNx7yBMFeN7d8` + +3. [微信](https://pb.yym68686.top/~wechat) + +4. [支付宝](https://pb.yym68686.top/~alipay) + +感谢您的支持! + +## 常见问题 + +- 为什么总是出现 `Error processing request or performing moral check: 404: No matching model found` 错误? + +将 ENABLE_MODERATION 设置为 false 将修复这个问题。当 ENABLE_MODERATION 为 true 时,API 必须能够使用 text-moderation-latest 模型,如果你没有在提供商模型设置里面提供 text-moderation-latest,将会报错找不到模型。 + +- 怎么优先请求某个渠道,怎么设置渠道的优先级? + +直接在api_keys里面通过设置渠道顺序即可。不需要做其他设置,示例配置文件: + +```yaml +providers: + - provider: ai1 + base_url: https://xxx/v1/chat/completions + api: sk-xxx + + - provider: ai2 + base_url: https://xxx/v1/chat/completions + api: sk-xxx + +api_keys: + - api: sk-1234 + model: + - ai2/* + - ai1/* +``` + +这样设置则先请求 ai2,失败后请求 ai1。 + +- 各种调度算法背后的行为是怎样的?比如 fixed_priority,weighted_round_robin,lottery,random,round_robin? + +所有调度算法需要通过在配置文件的 api_keys.(api).preferences.SCHEDULING_ALGORITHM 设置为 fixed_priority,weighted_round_robin,lottery,random,round_robin 中的任意值来开启。 + +1. fixed_priority:固定优先级调度。所有请求永远执行第一个拥有用户请求的模型的渠道。报错时,会切换下一个渠道。这是默认的调度算法。 + +2. weighted_round_robin:加权轮训负载均衡,按照配置文件 api_keys.(api).model 设定的权重顺序请求拥有用户请求的模型的渠道。 + +3. lottery:抽奖轮训负载均衡,按照配置文件 api_keys.(api).model 设置的权重随机请求拥有用户请求的模型的渠道。 + +4. round_robin:轮训负载均衡,按照配置文件 api_keys.(api).model 的配置顺序请求拥有用户请求的模型的渠道。可以查看上一个问题,如何设置渠道的优先级。 + +- 应该怎么正确填写 base_url? + +除了高级配置里面所展示的一些特殊的渠道,所有 OpenAI 格式的提供商需要把 base_url 填完整,也就是说 base_url 必须以 /v1/chat/completions 结尾。如果你使用的 GitHub models,base_url 应该填写为 https://models.inference.ai.azure.com/chat/completions,而不是 Azure 的 URL。 + +对于 Azure 渠道,base_url 兼容以下几种写法:https://your-endpoint.services.ai.azure.com/models/chat/completions?api-version=2024-05-01-preview 和 https://your-endpoint.services.ai.azure.com/models/chat/completions,https://your-endpoint.openai.azure.com,推荐使用第一种写法。如果不显式指定 api-version,默认使用 2024-10-21 版本。 + +- 模型超时时间是如何确认的?渠道级别的超时设置和全局模型超时设置的优先级是什么? + +渠道级别的超时设置优先级高于全局模型超时设置。优先级顺序:渠道级别模型超时设置 > 渠道级别默认超时设置 > 全局模型超时设置 > 全局默认超时设置 > 环境变量 TIMEOUT。 + +通过调整模型超时时间,可以避免出现某些渠道请求超时报错的情况。如果你遇到 `{'error': '500', 'details': 'fetch_response_stream Read Response Timeout'}` 错误,请尝试增加模型超时时间。 + +- api_key_rate_limit 是怎么工作的?我如何给多个模型设置相同的频率限制? + +如果你想同时给 gemini-1.5-pro-latest,gemini-1.5-pro,gemini-1.5-pro-001,gemini-1.5-pro-002 这四个模型设置相同的频率限制,可以这样设置: + +```yaml +api_key_rate_limit: + gemini-1.5-pro: 1000/min +``` + +这会匹配所有含有 gemini-1.5-pro 字符串的模型。gemini-1.5-pro-latest,gemini-1.5-pro,gemini-1.5-pro-001,gemini-1.5-pro-002 这四个模型频率限制都会设置为 1000/min。api_key_rate_limit 字段配置的逻辑如下,这是一个示例配置文件: + +```yaml +api_key_rate_limit: + gemini-1.5-pro: 1000/min + gemini-1.5-pro-002: 500/min +``` + +此时如果有一个使用模型 gemini-1.5-pro-002 的请求。 + +首先,uni-api 会尝试精确匹配 api_key_rate_limit 的模型。如果刚好设置了 gemini-1.5-pro-002 的频率限制,则 gemini-1.5-pro-002 的频率限制则为 500/min,如果此时请求的模型不是 gemini-1.5-pro-002,而是 gemini-1.5-pro-latest,由于 api_key_rate_limit 没有设置 gemini-1.5-pro-latest 的频率限制,因此会寻找有没有前缀和 gemini-1.5-pro-latest 相同的模型被设置了,因此 gemini-1.5-pro-latest 的频率限制会被设置为 1000/min。 + +- 我想设置渠道1和渠道2为随机轮训,uni-api 在渠道1和渠道2请求失败后才自动重试渠道3,怎么设置? + +uni-api 支持将 api key 本身作为渠道,可以通过这一特性对渠道进行分组管理。 + +```yaml +api_keys: + - api: sk-xxx1 + model: + - sk-xxx2/* # 渠道 1 2 采用随机轮训,失败后请求渠道3 + - aws/* # 渠道3 + preferences: + SCHEDULING_ALGORITHM: fixed_priority # 表示始终优先请求 api key:sk-xxx2 里面的渠道 1 2,失败后自动请求渠道 3 + + - api: sk-xxx2 + model: + - anthropic/claude-3-7-sonnet # 渠道1 + - openrouter/claude-3-7-sonnet # 渠道2 + preferences: + SCHEDULING_ALGORITHM: random # 渠道 1 2 采用随机轮训 +``` + +## ⭐ Star 历史 + + + Star History Chart + \ No newline at end of file diff --git a/uni-api/data.yml b/uni-api/data.yml new file mode 100644 index 000000000..461ac09b8 --- /dev/null +++ b/uni-api/data.yml @@ -0,0 +1,19 @@ +name: uni-api +tags: + - AI / 大模型 +title: 统一管理大模型 API 的项目,可以通过一个统一的API 接口调用多种不同提供商的服务,统一转换为 OpenAI 格式。 +description: 如果个人使用的话,one/new-api 过于复杂,有很多个人不需要使用的商用功能,如果你不想要复杂的前端界面,又想要支持的模型多一点,可以试试 uni-api。这是一个统一管理大模型 API 的项目,可以通过一个统一的API 接口调用多种不同提供商的服务,统一转换为 OpenAI 格式,支持负载均衡。目前支持的后端服务有:OpenAI、Anthropic、Gemini、Vertex、Azure、xai、Cohere、Groq、Cloudflare、OpenRouter 等。 +additionalProperties: + key: uni-api + name: uni api + tags: + - AI + shortDescZh: 这是一个统一管理大模型 API 的项目,可以通过一个统一的API 接口调用多种不同提供商的服务,统一转换为 OpenAI 格式,支持负载均衡。目前支持的后端服务有:OpenAI、Anthropic、Gemini、Vertex、Azure、xai、Cohere、Groq、Cloudflare、OpenRouter 等。 + shortDescEn: This is a project that unifies the management of LLM APIs. It can call multiple backend services through a unified API interface, convert them to the OpenAI format uniformly, and support load balancing. + type: tool + crossVersionUpdate: true + limit: 0 + recommend: 0 + website: https://github.com/yym68686/uni-api + github: https://github.com/yym68686/uni-api + document: https://github.com/yym68686/uni-api/blob/main/README_CN.md diff --git a/uni-api/latest/data.yml b/uni-api/latest/data.yml new file mode 100644 index 000000000..a030dceae --- /dev/null +++ b/uni-api/latest/data.yml @@ -0,0 +1,35 @@ +additionalProperties: + formFields: + - default: "" + edit: true + envKey: CONFIG_URL + labelEn: CONFIG_URL + labelZh: 远程配置文件地址(若配置,则首先拉取远程配置到本地) + required: false + rule: paramCommon + type: text + - default: "./api.yaml" + edit: true + envKey: LOCAL_CONFIG_PATH + labelEn: local config file path + labelZh: 本地配置文件地址 + required: true + rule: paramCommon + type: text + - default: "./data" + edit: true + envKey: DATA_PATH + labelEn: uni-api data path + labelZh: uni-api数据存储路径 + required: true + rule: paramCommon + type: text + - default: 48000 + edit: true + envKey: PANEL_APP_PORT_HTTP + labelEn: Port + labelZh: 端口 + required: true + rule: paramPort + type: number + diff --git a/uni-api/latest/docker-compose.yml b/uni-api/latest/docker-compose.yml new file mode 100644 index 000000000..6b922efbe --- /dev/null +++ b/uni-api/latest/docker-compose.yml @@ -0,0 +1,21 @@ +services: + uni-api: + image: yym68686/uni-api:latest + container_name: ${CONTAINER_NAME} + restart: always + ports: + - ${PANEL_APP_PORT_HTTP}:8000 + volumes: + - "${DATA_PATH}:/home/data" + - "${LOCAL_CONFIG_PATH}:/home/api.yaml" + environment: + - CONFIG_URL=${CONFIG_URL} + networks: + - 1panel-network + labels: + createdBy: "Apps" + + +networks: + 1panel-network: + external: true diff --git a/uni-api/logo.png b/uni-api/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..54a5336357ae91659ba3f8ab5a0394889cf1670d GIT binary patch literal 43853 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 Yv%?nK?>QseLg7bu=^E{r#T&i<2Z(>~BLDyZ literal 0 HcmV?d00001