Add feature: support load balancing for multiple keys in a single channel, enabled by default.
Browse files- README.md +4 -3
- request.py +6 -6
- test/test_nostream.py +121 -0
- utils.py +8 -0
README.md
CHANGED
@@ -21,10 +21,9 @@
|
|
21 |
- 同时支持 Anthropic、Gemini、Vertex API。Vertex 同时支持 Claude 和 Gemini API。
|
22 |
- 支持 OpenAI、 Anthropic、Gemini、Vertex 原生 tool use 函数调用。
|
23 |
- 支持 OpenAI、Anthropic、Gemini、Vertex 原生识图 API。
|
24 |
-
-
|
25 |
- 支持自动重试,当一个 API 渠道响应失败时,自动重试下一个 API 渠道。
|
26 |
- 支持细粒度的权限控制。支持使用通配符设置 API key 可用渠道的特定模型。
|
27 |
-
- 支持多个 API Key。
|
28 |
|
29 |
## Configuration
|
30 |
|
@@ -42,7 +41,9 @@ providers:
|
|
42 |
|
43 |
- provider: anthropic
|
44 |
base_url: https://api.anthropic.com/v1/messages
|
45 |
-
api:
|
|
|
|
|
46 |
model:
|
47 |
- claude-3-5-sonnet-20240620: claude-3-5-sonnet # 重命名模型,claude-3-5-sonnet-20240620 是服务商的模型名称,claude-3-5-sonnet 是重命名后的名字,可以使用简洁的名字代替原来复杂的名称,选填
|
48 |
tools: true # 是否支持工具,如生成代码、生成文档等,默认是 true,选填
|
|
|
21 |
- 同时支持 Anthropic、Gemini、Vertex API。Vertex 同时支持 Claude 和 Gemini API。
|
22 |
- 支持 OpenAI、 Anthropic、Gemini、Vertex 原生 tool use 函数调用。
|
23 |
- 支持 OpenAI、Anthropic、Gemini、Vertex 原生识图 API。
|
24 |
+
- 支持三种负载均衡,默认同时开启。1. 支持单个渠道多个 API Key 自动开启 API key 级别的轮训负载均衡。2. 支持 Vertex 区域级负载均衡,支持 Vertex 高并发,最高可将 Gemini,Claude 并发提高 (API数量 * 区域数量) 倍。3. 除了 Vertex 区域级负载均衡,所有 API 均支持渠道级负载均衡,提高沉浸式翻译体验。
|
25 |
- 支持自动重试,当一个 API 渠道响应失败时,自动重试下一个 API 渠道。
|
26 |
- 支持细粒度的权限控制。支持使用通配符设置 API key 可用渠道的特定模型。
|
|
|
27 |
|
28 |
## Configuration
|
29 |
|
|
|
41 |
|
42 |
- provider: anthropic
|
43 |
base_url: https://api.anthropic.com/v1/messages
|
44 |
+
api: # 支持多个 API Key,多个 key 自动开启轮训负载均衡,至少一个 key,必填
|
45 |
+
- sk-ant-api03-bNnAOJyA-xQw_twAA
|
46 |
+
- sk-ant-api02-bNnxxxx
|
47 |
model:
|
48 |
- claude-3-5-sonnet-20240620: claude-3-5-sonnet # 重命名模型,claude-3-5-sonnet-20240620 是服务商的模型名称,claude-3-5-sonnet 是重命名后的名字,可以使用简洁的名字代替原来复杂的名称,选填
|
49 |
tools: true # 是否支持工具,如生成代码、生成文档等,默认是 true,选填
|
request.py
CHANGED
@@ -43,9 +43,9 @@ async def get_gemini_payload(request, engine, provider):
|
|
43 |
gemini_stream = "streamGenerateContent"
|
44 |
url = provider['base_url']
|
45 |
if url.endswith("v1beta"):
|
46 |
-
url = "https://generativelanguage.googleapis.com/v1beta/models/{model}:{stream}?key={api_key}".format(model=model, stream=gemini_stream, api_key=provider['api'])
|
47 |
if url.endswith("v1"):
|
48 |
-
url = "https://generativelanguage.googleapis.com/v1/models/{model}:{stream}?key={api_key}".format(model=model, stream=gemini_stream, api_key=provider['api'])
|
49 |
|
50 |
messages = []
|
51 |
systemInstruction = None
|
@@ -492,7 +492,7 @@ async def get_gpt_payload(request, engine, provider):
|
|
492 |
'Content-Type': 'application/json',
|
493 |
}
|
494 |
if provider.get("api"):
|
495 |
-
headers['Authorization'] = f"Bearer {provider['api']}"
|
496 |
url = provider['base_url']
|
497 |
|
498 |
messages = []
|
@@ -556,7 +556,7 @@ async def get_openrouter_payload(request, engine, provider):
|
|
556 |
'Content-Type': 'application/json'
|
557 |
}
|
558 |
if provider.get("api"):
|
559 |
-
headers['Authorization'] = f"Bearer {provider['api']}"
|
560 |
|
561 |
url = provider['base_url']
|
562 |
|
@@ -640,7 +640,7 @@ async def get_claude_payload(request, engine, provider):
|
|
640 |
model = provider['model'][request.model]
|
641 |
headers = {
|
642 |
"content-type": "application/json",
|
643 |
-
"x-api-key": f"{provider['api']}",
|
644 |
"anthropic-version": "2023-06-01",
|
645 |
"anthropic-beta": "max-tokens-3-5-sonnet-2024-07-15" if "claude-3-5-sonnet" in model else "tools-2024-05-16",
|
646 |
}
|
@@ -753,7 +753,7 @@ async def get_dalle_payload(request, engine, provider):
|
|
753 |
"Content-Type": "application/json",
|
754 |
}
|
755 |
if provider.get("api"):
|
756 |
-
headers['Authorization'] = f"Bearer {provider['api']}"
|
757 |
url = provider['base_url']
|
758 |
url = BaseAPI(url).image_url
|
759 |
|
|
|
43 |
gemini_stream = "streamGenerateContent"
|
44 |
url = provider['base_url']
|
45 |
if url.endswith("v1beta"):
|
46 |
+
url = "https://generativelanguage.googleapis.com/v1beta/models/{model}:{stream}?key={api_key}".format(model=model, stream=gemini_stream, api_key=provider['api'].next())
|
47 |
if url.endswith("v1"):
|
48 |
+
url = "https://generativelanguage.googleapis.com/v1/models/{model}:{stream}?key={api_key}".format(model=model, stream=gemini_stream, api_key=provider['api'].next())
|
49 |
|
50 |
messages = []
|
51 |
systemInstruction = None
|
|
|
492 |
'Content-Type': 'application/json',
|
493 |
}
|
494 |
if provider.get("api"):
|
495 |
+
headers['Authorization'] = f"Bearer {provider['api'].next()}"
|
496 |
url = provider['base_url']
|
497 |
|
498 |
messages = []
|
|
|
556 |
'Content-Type': 'application/json'
|
557 |
}
|
558 |
if provider.get("api"):
|
559 |
+
headers['Authorization'] = f"Bearer {provider['api'].next()}"
|
560 |
|
561 |
url = provider['base_url']
|
562 |
|
|
|
640 |
model = provider['model'][request.model]
|
641 |
headers = {
|
642 |
"content-type": "application/json",
|
643 |
+
"x-api-key": f"{provider['api'].next()}",
|
644 |
"anthropic-version": "2023-06-01",
|
645 |
"anthropic-beta": "max-tokens-3-5-sonnet-2024-07-15" if "claude-3-5-sonnet" in model else "tools-2024-05-16",
|
646 |
}
|
|
|
753 |
"Content-Type": "application/json",
|
754 |
}
|
755 |
if provider.get("api"):
|
756 |
+
headers['Authorization'] = f"Bearer {provider['api'].next()}"
|
757 |
url = provider['base_url']
|
758 |
url = BaseAPI(url).image_url
|
759 |
|
test/test_nostream.py
ADDED
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import requests
|
2 |
+
import base64
|
3 |
+
import json
|
4 |
+
import os
|
5 |
+
from datetime import datetime
|
6 |
+
|
7 |
+
# 設置API密鑰和自定義base URL
|
8 |
+
API_KEY = ''
|
9 |
+
BASE_URL = 'http://localhost:8000/v1'
|
10 |
+
SAVE_DIR = 'safe_output' # 保存 JSON 輸出的目錄
|
11 |
+
|
12 |
+
def ensure_save_directory():
|
13 |
+
if not os.path.exists(SAVE_DIR):
|
14 |
+
os.makedirs(SAVE_DIR)
|
15 |
+
|
16 |
+
def image_to_base64(image_path):
|
17 |
+
with open(image_path, "rb") as image_file:
|
18 |
+
return base64.b64encode(image_file.read()).decode('utf-8')
|
19 |
+
|
20 |
+
def get_model_response(image_base64):
|
21 |
+
headers = {
|
22 |
+
"Content-Type": "application/json",
|
23 |
+
"Authorization": f"Bearer {API_KEY}"
|
24 |
+
}
|
25 |
+
|
26 |
+
tools = [
|
27 |
+
{
|
28 |
+
"type": "function",
|
29 |
+
"function": {
|
30 |
+
"name": "extract_underlined_text",
|
31 |
+
"description": "從圖片中提取紅色下劃線的文字",
|
32 |
+
"parameters": {
|
33 |
+
"type": "object",
|
34 |
+
"properties": {
|
35 |
+
"underlined_text": {
|
36 |
+
"type": "array",
|
37 |
+
"items": {"type": "string"},
|
38 |
+
"description": "紅色下劃線的文字列表"
|
39 |
+
}
|
40 |
+
},
|
41 |
+
"required": ["underlined_text"]
|
42 |
+
}
|
43 |
+
}
|
44 |
+
}
|
45 |
+
]
|
46 |
+
|
47 |
+
payload = {
|
48 |
+
|
49 |
+
"model": "claude-3-5-sonnet",
|
50 |
+
"messages": [
|
51 |
+
{
|
52 |
+
"role": "user",
|
53 |
+
"content": [
|
54 |
+
{
|
55 |
+
"type": "text",
|
56 |
+
"text": "請仔細分析圖片,並提取所有使用紅色筆在單字、單詞或句子下方畫有橫線的文字。只提取有紅色下劃線的文字,忽略其他未標記的文字。將結果以 JSON 格式輸出,格式為 {\"underlined_text\": [\"文字1\", \"文字2\", ...]}。"
|
57 |
+
},
|
58 |
+
{
|
59 |
+
"type": "image_url",
|
60 |
+
"image_url": {
|
61 |
+
"url": f"data:image/jpeg;base64,{image_base64}"
|
62 |
+
}
|
63 |
+
}
|
64 |
+
]
|
65 |
+
}
|
66 |
+
],
|
67 |
+
"stream": True,
|
68 |
+
"tools": tools,
|
69 |
+
"tool_choice": {"type": "function", "function": {"name": "extract_underlined_text"}},
|
70 |
+
"max_tokens": 300
|
71 |
+
}
|
72 |
+
|
73 |
+
try:
|
74 |
+
response = requests.post(f"{BASE_URL}/chat/completions", headers=headers, json=payload, timeout=30)
|
75 |
+
response.raise_for_status()
|
76 |
+
return response.json()
|
77 |
+
except requests.exceptions.RequestException as e:
|
78 |
+
return f"Error: {e}"
|
79 |
+
|
80 |
+
def save_json_output(data):
|
81 |
+
ensure_save_directory()
|
82 |
+
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
|
83 |
+
filename = f"{SAVE_DIR}/output_{timestamp}.json"
|
84 |
+
with open(filename, 'w', encoding='utf-8') as f:
|
85 |
+
json.dump(data, f, ensure_ascii=False, indent=2)
|
86 |
+
return filename
|
87 |
+
|
88 |
+
def main(image_path):
|
89 |
+
image_base64 = image_to_base64(image_path)
|
90 |
+
|
91 |
+
response = get_model_response(image_base64)
|
92 |
+
|
93 |
+
print("模型回應:")
|
94 |
+
print(json.dumps(response, indent=2, ensure_ascii=False))
|
95 |
+
|
96 |
+
if isinstance(response, str) and response.startswith("Error"):
|
97 |
+
print(response)
|
98 |
+
return
|
99 |
+
|
100 |
+
if 'choices' in response and response['choices']:
|
101 |
+
message = response['choices'][0]['message']
|
102 |
+
if 'tool_calls' in message:
|
103 |
+
tool_call = message['tool_calls'][0]
|
104 |
+
if tool_call['function']['name'] == 'extract_underlined_text':
|
105 |
+
function_args = json.loads(tool_call['function']['arguments'])
|
106 |
+
print("\n提取的紅色下劃線文字:")
|
107 |
+
print(json.dumps(function_args, indent=2, ensure_ascii=False))
|
108 |
+
|
109 |
+
# 保存 JSON 輸出
|
110 |
+
saved_file = save_json_output(function_args)
|
111 |
+
print(f"\nJSON 輸出已保存至: {saved_file}")
|
112 |
+
else:
|
113 |
+
print("\n模型調用了未預期的函數。")
|
114 |
+
else:
|
115 |
+
print("\n模型沒有調用工具。")
|
116 |
+
else:
|
117 |
+
print("\n無法解析回應。")
|
118 |
+
|
119 |
+
if __name__ == "__main__":
|
120 |
+
image_path = "00001 (8).jpg" # 替換為您的圖像路徑
|
121 |
+
main(image_path)
|
utils.py
CHANGED
@@ -15,7 +15,15 @@ def update_config(config_data):
|
|
15 |
provider['model'] = model_dict
|
16 |
if provider.get('project_id'):
|
17 |
provider['base_url'] = 'https://aiplatform.googleapis.com/'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
18 |
config_data['providers'][index] = provider
|
|
|
19 |
api_keys_db = config_data['api_keys']
|
20 |
api_list = [item["api"] for item in api_keys_db]
|
21 |
# logger.info(json.dumps(config_data, indent=4, ensure_ascii=False))
|
|
|
15 |
provider['model'] = model_dict
|
16 |
if provider.get('project_id'):
|
17 |
provider['base_url'] = 'https://aiplatform.googleapis.com/'
|
18 |
+
|
19 |
+
if provider.get('api'):
|
20 |
+
if isinstance(provider.get('api'), str):
|
21 |
+
provider['api'] = CircularList([provider.get('api')])
|
22 |
+
if isinstance(provider.get('api'), list):
|
23 |
+
provider['api'] = CircularList(provider.get('api'))
|
24 |
+
|
25 |
config_data['providers'][index] = provider
|
26 |
+
|
27 |
api_keys_db = config_data['api_keys']
|
28 |
api_list = [item["api"] for item in api_keys_db]
|
29 |
# logger.info(json.dumps(config_data, indent=4, ensure_ascii=False))
|