🤖 Models: Add support for the cohere series models
Browse files- README.md +3 -3
- README_CN.md +3 -3
- main.py +5 -1
- request.py +75 -5
- response.py +26 -0
- test/test_matplotlib.py +72 -1
README.md
CHANGED
@@ -13,13 +13,13 @@
|
|
13 |
|
14 |
## Introduction
|
15 |
|
16 |
-
If used
|
17 |
|
18 |
## Features
|
19 |
|
20 |
- No frontend, pure configuration file setup for API channels. You can run your own API site by just writing one file, with detailed configuration guides in the documentation, beginner-friendly.
|
21 |
- Unified management of multiple backend services, supporting providers like OpenAI, Deepseek, DeepBricks, OpenRouter, and other APIs in the OpenAI format. Supports OpenAI Dalle-3 image generation.
|
22 |
-
- Supports Anthropic, Gemini, Vertex
|
23 |
- Supports OpenAI, Anthropic, Gemini, Vertex native tool use function calls.
|
24 |
- Supports OpenAI, Anthropic, Gemini, Vertex native image recognition API.
|
25 |
- Supports four types of load balancing.
|
@@ -125,7 +125,7 @@ api_keys:
|
|
125 |
## Environment Variables
|
126 |
|
127 |
- CONFIG_URL: The download address of the configuration file, it can be a local file or a remote file, optional
|
128 |
-
- TIMEOUT: Request timeout, default is
|
129 |
|
130 |
## Docker Local Deployment
|
131 |
|
|
|
13 |
|
14 |
## Introduction
|
15 |
|
16 |
+
If used for personal purposes, one/new-api is too complex and has many commercial features that individuals do not need. If you do not want a complex front-end interface and want to support more models, you can try uni-api. This is a project that manages large model APIs uniformly and allows you to call multiple backend services through a unified API interface, converting them uniformly to the OpenAI format and supporting load balancing. The currently supported backend services include: OpenAI, Anthropic, Gemini, Vertex, Cohere, Cloudflare, DeepBricks, OpenRouter, etc.
|
17 |
|
18 |
## Features
|
19 |
|
20 |
- No frontend, pure configuration file setup for API channels. You can run your own API site by just writing one file, with detailed configuration guides in the documentation, beginner-friendly.
|
21 |
- Unified management of multiple backend services, supporting providers like OpenAI, Deepseek, DeepBricks, OpenRouter, and other APIs in the OpenAI format. Supports OpenAI Dalle-3 image generation.
|
22 |
+
- Supports Anthropic, Gemini, Vertex AI, Cohere, Cloudflare. Vertex supports both Claude and Gemini API.
|
23 |
- Supports OpenAI, Anthropic, Gemini, Vertex native tool use function calls.
|
24 |
- Supports OpenAI, Anthropic, Gemini, Vertex native image recognition API.
|
25 |
- Supports four types of load balancing.
|
|
|
125 |
## Environment Variables
|
126 |
|
127 |
- CONFIG_URL: The download address of the configuration file, it can be a local file or a remote file, optional
|
128 |
+
- TIMEOUT: Request timeout, default is 100 seconds, the timeout can control the time needed to switch to the next channel when a channel does not respond. Optional
|
129 |
|
130 |
## Docker Local Deployment
|
131 |
|
README_CN.md
CHANGED
@@ -13,13 +13,13 @@
|
|
13 |
|
14 |
## Introduction
|
15 |
|
16 |
-
如果个人使用的话,one/new-api 过于复杂,有很多个人不需要使用的商用功能,如果你不想要复杂的前端界面,有想要支持的模型多一点,可以试试 uni-api。这是一个统一管理大模型API的项目,可以通过一个统一的API接口调用多个后端服务,统一转换为 OpenAI 格式,支持负载均衡。目前支持的后端服务有:OpenAI、Anthropic、Gemini、Vertex、
|
17 |
|
18 |
## Features
|
19 |
|
20 |
- 无前端,纯配置文件配置 API 渠道。只要写一个文件就能运行起一个属于自己的 API 站,文档有详细的配置指南,小白友好。
|
21 |
- 统一管理多个后端服务,支持 OpenAI、Deepseek、DeepBricks、OpenRouter 等其他 API 是 OpenAI 格式的提供商。支持 OpenAI Dalle-3 图像生成。
|
22 |
-
- 同时支持 Anthropic、Gemini、Vertex
|
23 |
- 支持 OpenAI、 Anthropic、Gemini、Vertex 原生 tool use 函数调用。
|
24 |
- 支持 OpenAI、Anthropic、Gemini、Vertex 原生识图 API。
|
25 |
- 支持四种负载均衡。
|
@@ -125,7 +125,7 @@ api_keys:
|
|
125 |
## 环境变量
|
126 |
|
127 |
- CONFIG_URL: 配置文件的下载地址,可以是本地文件,也可以是远程文件,选填
|
128 |
-
- TIMEOUT: 请求超时时间,默认为
|
129 |
|
130 |
## Docker Local Deployment
|
131 |
|
|
|
13 |
|
14 |
## Introduction
|
15 |
|
16 |
+
如果个人使用的话,one/new-api 过于复杂,有很多个人不需要使用的商用功能,如果你不想要复杂的前端界面,有想要支持的模型多一点,可以试试 uni-api。这是一个统一管理大模型API的项目,可以通过一个统一的API接口调用多个后端服务,统一转换为 OpenAI 格式,支持负载均衡。目前支持的后端服务有:OpenAI、Anthropic、Gemini、Vertex、Cohere、Cloudflare、DeepBricks、OpenRouter 等。
|
17 |
|
18 |
## Features
|
19 |
|
20 |
- 无前端,纯配置文件配置 API 渠道。只要写一个文件就能运行起一个属于自己的 API 站,文档有详细的配置指南,小白友好。
|
21 |
- 统一管理多个后端服务,支持 OpenAI、Deepseek、DeepBricks、OpenRouter 等其他 API 是 OpenAI 格式的提供商。支持 OpenAI Dalle-3 图像生成。
|
22 |
+
- 同时支持 Anthropic、Gemini、Vertex AI、Cohere、Cloudflare。Vertex 同时支持 Claude 和 Gemini API。
|
23 |
- 支持 OpenAI、 Anthropic、Gemini、Vertex 原生 tool use 函数调用。
|
24 |
- 支持 OpenAI、Anthropic、Gemini、Vertex 原生识图 API。
|
25 |
- 支持四种负载均衡。
|
|
|
125 |
## 环境变量
|
126 |
|
127 |
- CONFIG_URL: 配置文件的下载地址,可以是本地文件,也可以是远程文件,选填
|
128 |
+
- TIMEOUT: 请求超时时间,默认为 100 秒,超时时间可以控制当一个渠道没有响应时,切换下一个渠道需要的时间。选填
|
129 |
|
130 |
## Docker Local Deployment
|
131 |
|
main.py
CHANGED
@@ -229,13 +229,17 @@ async def process_request(request: Union[RequestModel, ImageGenerationRequest],
|
|
229 |
engine = "claude"
|
230 |
elif parsed_url.netloc == 'openrouter.ai':
|
231 |
engine = "openrouter"
|
|
|
|
|
|
|
232 |
else:
|
233 |
engine = "gpt"
|
234 |
|
235 |
if "claude" not in provider['model'][request.model] \
|
236 |
and "gpt" not in provider['model'][request.model] \
|
237 |
and "gemini" not in provider['model'][request.model] \
|
238 |
-
and parsed_url.netloc != 'api.cloudflare.com'
|
|
|
239 |
engine = "openrouter"
|
240 |
|
241 |
if "claude" in provider['model'][request.model] and engine == "vertex":
|
|
|
229 |
engine = "claude"
|
230 |
elif parsed_url.netloc == 'openrouter.ai':
|
231 |
engine = "openrouter"
|
232 |
+
elif parsed_url.netloc == 'api.cohere.com':
|
233 |
+
engine = "cohere"
|
234 |
+
request.stream = True
|
235 |
else:
|
236 |
engine = "gpt"
|
237 |
|
238 |
if "claude" not in provider['model'][request.model] \
|
239 |
and "gpt" not in provider['model'][request.model] \
|
240 |
and "gemini" not in provider['model'][request.model] \
|
241 |
+
and parsed_url.netloc != 'api.cloudflare.com' \
|
242 |
+
and parsed_url.netloc != 'api.cohere.com':
|
243 |
engine = "openrouter"
|
244 |
|
245 |
if "claude" in provider['model'][request.model] and engine == "vertex":
|
request.py
CHANGED
@@ -1,12 +1,13 @@
|
|
1 |
import os
|
2 |
import re
|
3 |
import json
|
4 |
-
|
5 |
-
from utils import c35s, c3s, c3o, c3h, gem, BaseAPI
|
6 |
-
|
7 |
import base64
|
8 |
import urllib.parse
|
9 |
|
|
|
|
|
|
|
10 |
def encode_image(image_path):
|
11 |
with open(image_path, "rb") as image_file:
|
12 |
return base64.b64encode(image_file.read()).decode('utf-8')
|
@@ -82,6 +83,8 @@ async def get_text_message(role, message, engine = None):
|
|
82 |
return {"text": message}
|
83 |
if engine == "cloudflare":
|
84 |
return message
|
|
|
|
|
85 |
raise ValueError("Unknown engine")
|
86 |
|
87 |
async def get_gemini_payload(request, engine, provider):
|
@@ -215,8 +218,6 @@ async def get_gemini_payload(request, engine, provider):
|
|
215 |
return url, headers, payload
|
216 |
|
217 |
import time
|
218 |
-
import httpx
|
219 |
-
import base64
|
220 |
from cryptography.hazmat.primitives import hashes
|
221 |
from cryptography.hazmat.primitives.asymmetric import padding
|
222 |
from cryptography.hazmat.primitives.serialization import load_pem_private_key
|
@@ -690,6 +691,73 @@ async def get_openrouter_payload(request, engine, provider):
|
|
690 |
|
691 |
return url, headers, payload
|
692 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
693 |
async def get_cloudflare_payload(request, engine, provider):
|
694 |
headers = {
|
695 |
'Content-Type': 'application/json'
|
@@ -989,6 +1057,8 @@ async def get_payload(request: RequestModel, engine, provider):
|
|
989 |
return await get_cloudflare_payload(request, engine, provider)
|
990 |
elif engine == "o1":
|
991 |
return await get_o1_payload(request, engine, provider)
|
|
|
|
|
992 |
elif engine == "dalle":
|
993 |
return await get_dalle_payload(request, engine, provider)
|
994 |
else:
|
|
|
1 |
import os
|
2 |
import re
|
3 |
import json
|
4 |
+
import httpx
|
|
|
|
|
5 |
import base64
|
6 |
import urllib.parse
|
7 |
|
8 |
+
from models import RequestModel
|
9 |
+
from utils import c35s, c3s, c3o, c3h, gem, BaseAPI
|
10 |
+
|
11 |
def encode_image(image_path):
|
12 |
with open(image_path, "rb") as image_file:
|
13 |
return base64.b64encode(image_file.read()).decode('utf-8')
|
|
|
83 |
return {"text": message}
|
84 |
if engine == "cloudflare":
|
85 |
return message
|
86 |
+
if engine == "cohere":
|
87 |
+
return message
|
88 |
raise ValueError("Unknown engine")
|
89 |
|
90 |
async def get_gemini_payload(request, engine, provider):
|
|
|
218 |
return url, headers, payload
|
219 |
|
220 |
import time
|
|
|
|
|
221 |
from cryptography.hazmat.primitives import hashes
|
222 |
from cryptography.hazmat.primitives.asymmetric import padding
|
223 |
from cryptography.hazmat.primitives.serialization import load_pem_private_key
|
|
|
691 |
|
692 |
return url, headers, payload
|
693 |
|
694 |
+
async def get_cohere_payload(request, engine, provider):
|
695 |
+
headers = {
|
696 |
+
'Content-Type': 'application/json'
|
697 |
+
}
|
698 |
+
if provider.get("api"):
|
699 |
+
headers['Authorization'] = f"Bearer {provider['api'].next()}"
|
700 |
+
|
701 |
+
url = provider['base_url']
|
702 |
+
|
703 |
+
role_map = {
|
704 |
+
"user": "USER",
|
705 |
+
"assistant" : "CHATBOT",
|
706 |
+
"system": "SYSTEM"
|
707 |
+
}
|
708 |
+
|
709 |
+
messages = []
|
710 |
+
for msg in request.messages:
|
711 |
+
if isinstance(msg.content, list):
|
712 |
+
content = []
|
713 |
+
for item in msg.content:
|
714 |
+
if item.type == "text":
|
715 |
+
text_message = await get_text_message(msg.role, item.text, engine)
|
716 |
+
content.append(text_message)
|
717 |
+
else:
|
718 |
+
content = msg.content
|
719 |
+
|
720 |
+
if isinstance(content, list):
|
721 |
+
for item in content:
|
722 |
+
if item["type"] == "text":
|
723 |
+
messages.append({"role": role_map[msg.role], "message": item["text"]})
|
724 |
+
else:
|
725 |
+
messages.append({"role": role_map[msg.role], "message": content})
|
726 |
+
|
727 |
+
model = provider['model'][request.model]
|
728 |
+
chat_history = messages[:-1]
|
729 |
+
query = messages[-1].get("message")
|
730 |
+
payload = {
|
731 |
+
"model": model,
|
732 |
+
"message": query,
|
733 |
+
}
|
734 |
+
|
735 |
+
if chat_history:
|
736 |
+
payload["chat_history"] = chat_history
|
737 |
+
|
738 |
+
miss_fields = [
|
739 |
+
'model',
|
740 |
+
'messages',
|
741 |
+
'tools',
|
742 |
+
'tool_choice',
|
743 |
+
'temperature',
|
744 |
+
'top_p',
|
745 |
+
'max_tokens',
|
746 |
+
'presence_penalty',
|
747 |
+
'frequency_penalty',
|
748 |
+
'n',
|
749 |
+
'user',
|
750 |
+
'include_usage',
|
751 |
+
'logprobs',
|
752 |
+
'top_logprobs'
|
753 |
+
]
|
754 |
+
|
755 |
+
for field, value in request.model_dump(exclude_unset=True).items():
|
756 |
+
if field not in miss_fields and value is not None:
|
757 |
+
payload[field] = value
|
758 |
+
|
759 |
+
return url, headers, payload
|
760 |
+
|
761 |
async def get_cloudflare_payload(request, engine, provider):
|
762 |
headers = {
|
763 |
'Content-Type': 'application/json'
|
|
|
1057 |
return await get_cloudflare_payload(request, engine, provider)
|
1058 |
elif engine == "o1":
|
1059 |
return await get_o1_payload(request, engine, provider)
|
1060 |
+
elif engine == "cohere":
|
1061 |
+
return await get_cohere_payload(request, engine, provider)
|
1062 |
elif engine == "dalle":
|
1063 |
return await get_dalle_payload(request, engine, provider)
|
1064 |
else:
|
response.py
CHANGED
@@ -184,6 +184,29 @@ async def fetch_cloudflare_response_stream(client, url, headers, payload, model)
|
|
184 |
sse_string = await generate_sse_response(timestamp, model, content=message)
|
185 |
yield sse_string
|
186 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
187 |
async def fetch_claude_response_stream(client, url, headers, payload, model):
|
188 |
timestamp = int(datetime.timestamp(datetime.now()))
|
189 |
async with client.stream('POST', url, headers=headers, json=payload) as response:
|
@@ -270,6 +293,9 @@ async def fetch_response_stream(client, url, headers, payload, engine, model):
|
|
270 |
elif engine == "cloudflare":
|
271 |
async for chunk in fetch_cloudflare_response_stream(client, url, headers, payload, model):
|
272 |
yield chunk
|
|
|
|
|
|
|
273 |
else:
|
274 |
raise ValueError("Unknown response")
|
275 |
except httpx.ConnectError as e:
|
|
|
184 |
sse_string = await generate_sse_response(timestamp, model, content=message)
|
185 |
yield sse_string
|
186 |
|
187 |
+
async def fetch_cohere_response_stream(client, url, headers, payload, model):
|
188 |
+
timestamp = int(datetime.timestamp(datetime.now()))
|
189 |
+
async with client.stream('POST', url, headers=headers, json=payload) as response:
|
190 |
+
error_message = await check_response(response, "fetch_gpt_response_stream")
|
191 |
+
if error_message:
|
192 |
+
yield error_message
|
193 |
+
return
|
194 |
+
|
195 |
+
buffer = ""
|
196 |
+
async for chunk in response.aiter_text():
|
197 |
+
buffer += chunk
|
198 |
+
while "\n" in buffer:
|
199 |
+
line, buffer = buffer.split("\n", 1)
|
200 |
+
# logger.info("line: %s", repr(line))
|
201 |
+
resp: dict = json.loads(line)
|
202 |
+
if resp.get("is_finished") == True:
|
203 |
+
yield "data: [DONE]\n\r\n"
|
204 |
+
return
|
205 |
+
if resp.get("event_type") == "text-generation":
|
206 |
+
message = resp.get("text")
|
207 |
+
sse_string = await generate_sse_response(timestamp, model, content=message)
|
208 |
+
yield sse_string
|
209 |
+
|
210 |
async def fetch_claude_response_stream(client, url, headers, payload, model):
|
211 |
timestamp = int(datetime.timestamp(datetime.now()))
|
212 |
async with client.stream('POST', url, headers=headers, json=payload) as response:
|
|
|
293 |
elif engine == "cloudflare":
|
294 |
async for chunk in fetch_cloudflare_response_stream(client, url, headers, payload, model):
|
295 |
yield chunk
|
296 |
+
elif engine == "cohere":
|
297 |
+
async for chunk in fetch_cohere_response_stream(client, url, headers, payload, model):
|
298 |
+
yield chunk
|
299 |
else:
|
300 |
raise ValueError("Unknown response")
|
301 |
except httpx.ConnectError as e:
|
test/test_matplotlib.py
CHANGED
@@ -2,6 +2,7 @@ import json
|
|
2 |
import matplotlib.pyplot as plt
|
3 |
from datetime import datetime, timedelta
|
4 |
from collections import defaultdict
|
|
|
5 |
|
6 |
import matplotlib.font_manager as fm
|
7 |
font_path = '/System/Library/Fonts/PingFang.ttc'
|
@@ -45,5 +46,75 @@ def create_pic(request_arrivals, key):
|
|
45 |
# 保存图片
|
46 |
plt.savefig(f'{key.replace("/", "")}.png')
|
47 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
48 |
if __name__ == '__main__':
|
49 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2 |
import matplotlib.pyplot as plt
|
3 |
from datetime import datetime, timedelta
|
4 |
from collections import defaultdict
|
5 |
+
import numpy as np
|
6 |
|
7 |
import matplotlib.font_manager as fm
|
8 |
font_path = '/System/Library/Fonts/PingFang.ttc'
|
|
|
46 |
# 保存图片
|
47 |
plt.savefig(f'{key.replace("/", "")}.png')
|
48 |
|
49 |
+
def create_pie_chart(model_counts):
|
50 |
+
models = list(model_counts.keys())
|
51 |
+
counts = list(model_counts.values())
|
52 |
+
|
53 |
+
# 设置颜色和排列顺序
|
54 |
+
colors = plt.cm.Set3(np.linspace(0, 1, len(models)))
|
55 |
+
sorted_data = sorted(zip(counts, models, colors), reverse=True)
|
56 |
+
counts, models, colors = zip(*sorted_data)
|
57 |
+
|
58 |
+
# 创建饼图
|
59 |
+
fig, ax = plt.subplots(figsize=(16, 10))
|
60 |
+
wedges, _ = ax.pie(counts, colors=colors, startangle=90, wedgeprops=dict(width=0.5))
|
61 |
+
|
62 |
+
# 添加圆环效果
|
63 |
+
centre_circle = plt.Circle((0, 0), 0.35, fc='white')
|
64 |
+
fig.gca().add_artist(centre_circle)
|
65 |
+
|
66 |
+
# 计算总数
|
67 |
+
total = sum(counts)
|
68 |
+
|
69 |
+
# 准备标注
|
70 |
+
bbox_props = dict(boxstyle="round,pad=0.3", fc="w", ec="k", lw=0.72)
|
71 |
+
kw = dict(xycoords='data', textcoords='data', arrowprops=dict(arrowstyle="-"), bbox=bbox_props, zorder=0)
|
72 |
+
|
73 |
+
left_labels = []
|
74 |
+
right_labels = []
|
75 |
+
|
76 |
+
for i, p in enumerate(wedges):
|
77 |
+
ang = (p.theta2 - p.theta1) / 2. + p.theta1
|
78 |
+
y = np.sin(np.deg2rad(ang))
|
79 |
+
x = np.cos(np.deg2rad(ang))
|
80 |
+
|
81 |
+
percentage = counts[i] / total * 100
|
82 |
+
label = f"{models[i]}: {percentage:.1f}%"
|
83 |
+
|
84 |
+
if x > 0:
|
85 |
+
right_labels.append((x, y, label))
|
86 |
+
else:
|
87 |
+
left_labels.append((x, y, label))
|
88 |
+
|
89 |
+
# 绘制左侧标注
|
90 |
+
for i, (x, y, label) in enumerate(left_labels):
|
91 |
+
ax.annotate(label, xy=(x, y), xytext=(-1.2, 0.9 - i * 0.15), **kw)
|
92 |
+
|
93 |
+
# 绘制右侧标注
|
94 |
+
for i, (x, y, label) in enumerate(right_labels):
|
95 |
+
ax.annotate(label, xy=(x, y), xytext=(1.2, 0.9 - i * 0.15), **kw)
|
96 |
+
|
97 |
+
plt.title("各模型使用次数对比", size=16)
|
98 |
+
ax.set_xlim(-1.5, 1.5)
|
99 |
+
ax.set_ylim(-1.2, 1.2)
|
100 |
+
ax.axis('off')
|
101 |
+
plt.tight_layout()
|
102 |
+
plt.savefig('model_usage_pie_chart.png', bbox_inches='tight', pad_inches=0.5)
|
103 |
+
|
104 |
if __name__ == '__main__':
|
105 |
+
model_counts = {
|
106 |
+
"model_counts": {
|
107 |
+
"claude-3-5-sonnet": 94,
|
108 |
+
"o1-preview": 71,
|
109 |
+
"gpt-4o": 512,
|
110 |
+
"gpt-4o-mini": 5,
|
111 |
+
"gemini-1.5-pro": 5,
|
112 |
+
"deepseek-chat": 7,
|
113 |
+
"grok-2-mini": 1,
|
114 |
+
"grok-2": 9,
|
115 |
+
"o1-mini": 8
|
116 |
+
}
|
117 |
+
}
|
118 |
+
# create_pic(request_arrivals, 'POST /v1/chat/completions')
|
119 |
+
|
120 |
+
create_pie_chart(model_counts["model_counts"])
|