Spaces:
Sleeping
Sleeping
update
Browse files- api_key_sb.py +42 -19
- app.py +1 -0
- docs/api_key_management_solution_v1.md +260 -0
- proxy.py +3 -1
api_key_sb.py
CHANGED
|
@@ -20,30 +20,53 @@ def get_supabase_client() -> Client:
|
|
| 20 |
|
| 21 |
supabase: Client = get_supabase_client()
|
| 22 |
|
| 23 |
-
async def get_api_key_info(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
try:
|
| 25 |
-
#
|
| 26 |
-
|
| 27 |
-
response = supabase.from_('airs_model_api_keys_view').select('api_key', 'api_key_id').eq('model_name', model).order('api_key_ran_at', desc=False).limit(1).execute()
|
| 28 |
-
else:
|
| 29 |
-
# 如果没有提供model,则获取所有模型的api_key
|
| 30 |
-
raise HTTPException(status_code=400, detail="请提供模型名称!")
|
| 31 |
|
| 32 |
if response.data:
|
| 33 |
api_key_info = response.data[0]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
# 在返回api_key_info之前,更新其ran_at字段
|
| 35 |
-
await update_api_key_ran_at(
|
| 36 |
-
return
|
| 37 |
-
|
| 38 |
-
logger.error(f"未找到模型 '{model}' 的API密钥信息。")
|
| 39 |
-
raise HTTPException(status_code=404, detail=f"未找到模型 '{model}' 的API密钥信息。", headers={"X-Error-Type": "APIKeyNotFound"})
|
| 40 |
-
except HTTPException as e:
|
| 41 |
-
raise e # 重新抛出已有的HTTPException
|
| 42 |
except Exception as e:
|
| 43 |
-
|
| 44 |
-
|
| 45 |
|
| 46 |
-
async def update_api_key_ran_at(api_key_id:
|
| 47 |
"""
|
| 48 |
Updates the 'ran_at' field for a given API key in Supabase.
|
| 49 |
If ran_at_time is None, it defaults to the current Beijing time.
|
|
@@ -58,11 +81,11 @@ async def update_api_key_ran_at(api_key_id: str, ran_at_time: datetime = None):
|
|
| 58 |
current_local_time = ran_at_time.isoformat()
|
| 59 |
|
| 60 |
logger.info(f"尝试更新 API 密钥 {api_key_id} 的 ran_at 为 {current_local_time}")
|
| 61 |
-
response = supabase.table("
|
| 62 |
logger.info(f'API 密钥 {api_key_id} 的 ran_at 更新成功为 {current_local_time}. ')
|
| 63 |
|
| 64 |
# 验证更新是否成功
|
| 65 |
-
verification_response = supabase.from_('
|
| 66 |
verified_ran_at = verification_response.data.get('ran_at')
|
| 67 |
logger.info(f"验证:API 密钥 {api_key_id} 的实际 ran_at 值为 {verified_ran_at}")
|
| 68 |
|
|
|
|
| 20 |
|
| 21 |
supabase: Client = get_supabase_client()
|
| 22 |
|
| 23 |
+
# async def get_api_key_info(api_type_name: str, lookup_value: str):
|
| 24 |
+
# """
|
| 25 |
+
# 根据 API 类型名称和查找值从 Supabase 获取 API 密钥信息。
|
| 26 |
+
# """
|
| 27 |
+
# try:
|
| 28 |
+
# # 直接从 airs_model_api_keys_view_v1 视图中查询
|
| 29 |
+
# response = supabase.from_('airs_model_api_keys_view_v1').select('api_key, api_key_id').eq('api_type', api_type_name).eq('model_name', lookup_value).order('api_key_created_at', desc=False).limit(1).execute()
|
| 30 |
+
|
| 31 |
+
# if response.data:
|
| 32 |
+
# api_key_info = response.data[0]
|
| 33 |
+
# # 视图返回的 id 是 api_key_id,需要将其映射为 'id' 以保持与之前逻辑的兼容性
|
| 34 |
+
# formatted_api_key_info = {
|
| 35 |
+
# 'api_key': api_key_info.get('api_key'),
|
| 36 |
+
# 'id': api_key_info.get('api_key_id')
|
| 37 |
+
# }
|
| 38 |
+
# # 在返回api_key_info之前,更新其ran_at字段
|
| 39 |
+
# await update_api_key_ran_at(formatted_api_key_info.get('id'))
|
| 40 |
+
# return formatted_api_key_info
|
| 41 |
+
# return None
|
| 42 |
+
# except Exception as e:
|
| 43 |
+
# logger.error(f"Error fetching API key info from view: {e}")
|
| 44 |
+
# return None
|
| 45 |
+
|
| 46 |
+
async def get_api_key_info(lookup_value: str):
|
| 47 |
+
"""
|
| 48 |
+
根据 API 类型名称和查找值从 Supabase 获取 API 密钥信息。
|
| 49 |
+
"""
|
| 50 |
try:
|
| 51 |
+
# 直接从 airs_model_api_keys_view_v1 视图中查询
|
| 52 |
+
response = supabase.from_('airs_model_api_keys_view_v1').select('api_key, api_key_id').eq('model_name', lookup_value).order('api_key_created_at', desc=False).limit(1).execute()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
|
| 54 |
if response.data:
|
| 55 |
api_key_info = response.data[0]
|
| 56 |
+
# 视图返回的 id 是 api_key_id,需要将其映射为 'id' 以保持与之前逻辑的兼容性
|
| 57 |
+
formatted_api_key_info = {
|
| 58 |
+
'api_key': api_key_info.get('api_key'),
|
| 59 |
+
'id': api_key_info.get('api_key_id')
|
| 60 |
+
}
|
| 61 |
# 在返回api_key_info之前,更新其ran_at字段
|
| 62 |
+
await update_api_key_ran_at(formatted_api_key_info.get('id'))
|
| 63 |
+
return formatted_api_key_info
|
| 64 |
+
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
except Exception as e:
|
| 66 |
+
logger.error(f"Error fetching API key info from view: {e}")
|
| 67 |
+
return None
|
| 68 |
|
| 69 |
+
async def update_api_key_ran_at(api_key_id: int, ran_at_time: datetime = None):
|
| 70 |
"""
|
| 71 |
Updates the 'ran_at' field for a given API key in Supabase.
|
| 72 |
If ran_at_time is None, it defaults to the current Beijing time.
|
|
|
|
| 81 |
current_local_time = ran_at_time.isoformat()
|
| 82 |
|
| 83 |
logger.info(f"尝试更新 API 密钥 {api_key_id} 的 ran_at 为 {current_local_time}")
|
| 84 |
+
response = supabase.table("airs_api_keys_v1").update({"ran_at": current_local_time}).eq("id", api_key_id).execute()
|
| 85 |
logger.info(f'API 密钥 {api_key_id} 的 ran_at 更新成功为 {current_local_time}. ')
|
| 86 |
|
| 87 |
# 验证更新是否成功
|
| 88 |
+
verification_response = supabase.from_('airs_api_keys_v1').select('ran_at').eq('id', api_key_id).single().execute()
|
| 89 |
verified_ran_at = verification_response.data.get('ran_at')
|
| 90 |
logger.info(f"验证:API 密钥 {api_key_id} 的实际 ran_at 值为 {verified_ran_at}")
|
| 91 |
|
app.py
CHANGED
|
@@ -54,6 +54,7 @@ async def health_check():
|
|
| 54 |
# @app.api_route("/v1/{protocol}/{host}/{path:path}", methods=["POST"])
|
| 55 |
async def proxy(request: Request, protocol: ProtocolType, host:str, path: str, proxy_api_key: str = Depends(verify_proxy_api_key)): # 添加代理认证依赖
|
| 56 |
real_url = f"{protocol.value}://{host}/{path}"
|
|
|
|
| 57 |
# 提取客户端请求的headers和body
|
| 58 |
client_headers = dict(request.headers)
|
| 59 |
client_body = await request.body()
|
|
|
|
| 54 |
# @app.api_route("/v1/{protocol}/{host}/{path:path}", methods=["POST"])
|
| 55 |
async def proxy(request: Request, protocol: ProtocolType, host:str, path: str, proxy_api_key: str = Depends(verify_proxy_api_key)): # 添加代理认证依赖
|
| 56 |
real_url = f"{protocol.value}://{host}/{path}"
|
| 57 |
+
|
| 58 |
# 提取客户端请求的headers和body
|
| 59 |
client_headers = dict(request.headers)
|
| 60 |
client_body = await request.body()
|
docs/api_key_management_solution_v1.md
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# API 密钥管理方案
|
| 2 |
+
|
| 3 |
+
本文档概述了 `airs_api_keys` 表的数据库存储、查询和代码建议方案,以支持 LLM 和搜索引擎 API 密钥的灵活管理。
|
| 4 |
+
|
| 5 |
+
## 1. `airs_api_keys` 表的原始结构
|
| 6 |
+
|
| 7 |
+
```sql
|
| 8 |
+
create table public.airs_api_keys (
|
| 9 |
+
id bigint generated by default as identity not null,
|
| 10 |
+
created_at timestamp with time zone not null default now(),
|
| 11 |
+
api_key text null,
|
| 12 |
+
ran_at timestamp without time zone null default now(),
|
| 13 |
+
provider_id bigint null,
|
| 14 |
+
constraint airs_api_keys_pkey primary key (id),
|
| 15 |
+
constraint airs_api_keys_provider_id_fkey foreign KEY (provider_id) references airs_api_providers (id)
|
| 16 |
+
) TABLESPACE pg_default;
|
| 17 |
+
```
|
| 18 |
+
|
| 19 |
+
## 2. `airs_api_keys` 表的建议修改结构
|
| 20 |
+
|
| 21 |
+
为了支持不同类型的 API 密钥(LLM 和搜索引擎)以及更灵活的查找机制,建议修改 `airs_api_keys` 表结构如下:
|
| 22 |
+
|
| 23 |
+
```sql
|
| 24 |
+
create table public.airs_api_keys (
|
| 25 |
+
id bigint generated by default as identity not null,
|
| 26 |
+
created_at timestamp with time zone not null default now(),
|
| 27 |
+
api_key text null,
|
| 28 |
+
ran_at timestamp without time zone null default now(),
|
| 29 |
+
provider_id bigint null,
|
| 30 |
+
api_type text not null, -- 'llm' or 'search'
|
| 31 |
+
lookup_key text not null, -- LLM 的模型名称 (e.g., 'glm-4', 'gemini-pro'), 搜索引擎的域名或特定路径 (e.g., 'api.tavily.com')
|
| 32 |
+
url_pattern text null, -- 可选: 用于验证或更精确匹配的 URL 模式 (e.g., 'https://open.bigmodel.cn/api/paas/v4/chat/completions')
|
| 33 |
+
constraint airs_api_keys_pkey primary key (id),
|
| 34 |
+
constraint airs_api_keys_provider_id_fkey foreign KEY (provider_id) references airs_api_providers (id),
|
| 35 |
+
constraint airs_api_keys_unique_lookup_key unique (api_type, lookup_key) -- 确保每种类型和查找键组合的唯一性
|
| 36 |
+
) TABLESPACE pg_default;
|
| 37 |
+
```
|
| 38 |
+
|
| 39 |
+
**字段说明:**
|
| 40 |
+
|
| 41 |
+
* `api_type`: 字符串类型,用于区分 API 密钥的类型,例如 `'llm'` 或 `'search'`。
|
| 42 |
+
* `lookup_key`: 字符串类型,作为查询 API 密钥的关键字段。
|
| 43 |
+
* 对于 LLM 类 API,这将是请求体中 `model` 字段的值(例如 `glm-4`, `gemini-pro`)。
|
| 44 |
+
* 对于搜索引擎类 API,这将是其域名或特定路径(例如 `api.tavily.com`)。
|
| 45 |
+
* `url_pattern`: 可选的字符串类型,用于存储与 API 密钥关联的完整或部分 URL 模式。这可以用于在某些情况下进行额外的验证或更精确的匹配。
|
| 46 |
+
* `constraint airs_api_keys_unique_lookup_key`: 确保 `api_type` 和 `lookup_key` 的组合是唯一的,防止重复的 API 密钥配置。
|
| 47 |
+
|
| 48 |
+
### 示例数据库记录
|
| 49 |
+
|
| 50 |
+
以下是一些 `airs_api_keys` 表中可能存在的示例记录:
|
| 51 |
+
|
| 52 |
+
| id | created_at | api_key | ran_at | provider_id | api_type | lookup_key | url_pattern |
|
| 53 |
+
| --- | ---------------------- | -------------------------- | ------------------- | ----------- | -------- | -------------- | ------------------------------------------------------------------------ |
|
| 54 |
+
| 1 | 2023-01-01 10:00:00+00 | sk-xxxxxxxxxxxxxxxxxxxxglm | 2023-01-01 10:00:00 | 1 | llm | glm-4 | https://open.bigmodel.cn/api/paas/v4/chat/completions |
|
| 55 |
+
| 2 | 2023-01-02 11:00:00+00 | AIzaSyBxxxxxxxxxxxxxxxxxx | 2023-01-02 11:00:00 | 2 | llm | gemini-pro | https://generativelanguage.googleapis.com/v1beta/openai/chat/completions |
|
| 56 |
+
| 3 | 2023-01-03 12:00:00+00 | tvly-xxxxxxxxxxxxxxxxxxxx | 2023-01-03 12:00:00 | 3 | search | api.tavily.com | https://api.tavily.com/search |
|
| 57 |
+
|
| 58 |
+
## 3. 数据库查询方案
|
| 59 |
+
|
| 60 |
+
根据修改后的表结构,可以按 `api_type` 和 `lookup_key` 高效地查询 API 密钥。
|
| 61 |
+
|
| 62 |
+
### 查询 LLM API 密钥
|
| 63 |
+
|
| 64 |
+
当代理服务收到 LLM 请求时,它会从请求体中提取 `model` 名称,并使用它作为 `lookup_key` 进行查询。
|
| 65 |
+
|
| 66 |
+
```sql
|
| 67 |
+
SELECT api_key, provider_id
|
| 68 |
+
FROM public.airs_api_keys
|
| 69 |
+
WHERE api_type = 'llm' AND lookup_key = :model_name;
|
| 70 |
+
```
|
| 71 |
+
|
| 72 |
+
* `:model_name` 将替换为从请求体中解析出的模型名称,例如 `'glm-4'` 或 `'gemini-pro'`。
|
| 73 |
+
|
| 74 |
+
### 查询搜索引擎 API 密钥
|
| 75 |
+
|
| 76 |
+
当代理服务收到搜索引擎请求时,它会根据请求的 URL 识别出对应的域名或特定路径,并使用它作为 `lookup_key` 进行查询。
|
| 77 |
+
|
| 78 |
+
```sql
|
| 79 |
+
SELECT api_key, provider_id
|
| 80 |
+
FROM public.airs_api_keys
|
| 81 |
+
WHERE api_type = 'search' AND lookup_key = :domain_or_path;
|
| 82 |
+
```
|
| 83 |
+
|
| 84 |
+
* `:domain_or_path` 将替换为从请求 URL 中提取的域名或路径,例如 `'api.tavily.com'`。
|
| 85 |
+
|
| 86 |
+
## 4. 代码建议方案
|
| 87 |
+
|
| 88 |
+
为了在 FastAPI 代理服务中实现 API 密钥的动态获取和注入,需要修改 `api_key_sb.py` 和 `proxy.py` 文件。
|
| 89 |
+
|
| 90 |
+
### `api_key_sb.py` 中的 `get_api_key_info` 函数
|
| 91 |
+
|
| 92 |
+
修改 `get_api_key_info` 函数以接受 `api_type` 和 `lookup_value` 作为参数,并根据这些参数从 Supabase 查询 API 密钥。
|
| 93 |
+
|
| 94 |
+
```python
|
| 95 |
+
# api_key_sb.py
|
| 96 |
+
|
| 97 |
+
from supabase import create_client, Client
|
| 98 |
+
import os
|
| 99 |
+
from dotenv import load_dotenv
|
| 100 |
+
|
| 101 |
+
load_dotenv()
|
| 102 |
+
|
| 103 |
+
SUPABASE_URL = os.getenv("SUPABASE_URL")
|
| 104 |
+
SUPABASE_KEY = os.getenv("SUPABASE_KEY")
|
| 105 |
+
|
| 106 |
+
supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
|
| 107 |
+
|
| 108 |
+
async def get_api_key_info(api_type: str, lookup_value: str):
|
| 109 |
+
"""
|
| 110 |
+
根据 API 类型和查找值从 Supabase 获取 API 密钥信息。
|
| 111 |
+
"""
|
| 112 |
+
try:
|
| 113 |
+
response = supabase.from_('airs_api_keys').select('api_key, provider_id').eq('api_type', api_type).eq('lookup_key', lookup_value).limit(1).execute()
|
| 114 |
+
if response.data:
|
| 115 |
+
return response.data[0]
|
| 116 |
+
return None
|
| 117 |
+
except Exception as e:
|
| 118 |
+
print(f"Error fetching API key info: {e}")
|
| 119 |
+
return None
|
| 120 |
+
```
|
| 121 |
+
|
| 122 |
+
### `proxy.py` 中的请求处理逻辑
|
| 123 |
+
|
| 124 |
+
在 `proxy.py` 中,需要修改请求处理逻辑以:
|
| 125 |
+
|
| 126 |
+
1. 识别传入请求的 API 类型(LLM 或搜索引擎)。
|
| 127 |
+
2. 根据 API 类型从请求中提取相应的 `lookup_value`(`model` 名称或域名/路径)。
|
| 128 |
+
3. 调用 `get_api_key_info` 获取 API 密钥。
|
| 129 |
+
4. 将获取到的 API 密钥注入到转发请求的相应位置(例如,请求头或请求体)。
|
| 130 |
+
|
| 131 |
+
以下是 `proxy.py` 中 `handle_proxy_request` 函数的伪代码片段:
|
| 132 |
+
|
| 133 |
+
```python
|
| 134 |
+
# proxy.py
|
| 135 |
+
|
| 136 |
+
from fastapi import FastAPI, Request, Response, HTTPException
|
| 137 |
+
from fastapi.responses import StreamingResponse
|
| 138 |
+
import httpx
|
| 139 |
+
import json
|
| 140 |
+
import logging
|
| 141 |
+
import re
|
| 142 |
+
from api_key_sb import get_api_key_info # 导入新的函数
|
| 143 |
+
|
| 144 |
+
# 配置日志
|
| 145 |
+
logging.basicConfig(level=logging.INFO)
|
| 146 |
+
logger = logging.getLogger(__name__)
|
| 147 |
+
|
| 148 |
+
app = FastAPI()
|
| 149 |
+
client = httpx.AsyncClient()
|
| 150 |
+
|
| 151 |
+
# 统一错误响应格式
|
| 152 |
+
def create_error_response(status_code: int, message: str):
|
| 153 |
+
return Response(
|
| 154 |
+
content=json.dumps({"error": {"message": message, "type": "proxy_error"}}),
|
| 155 |
+
status_code=status_code,
|
| 156 |
+
media_type="application/json"
|
| 157 |
+
)
|
| 158 |
+
|
| 159 |
+
@app.api_route("/v1/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD", "PATCH", "TRACE"])
|
| 160 |
+
async def handle_proxy_request(request: Request, path: str):
|
| 161 |
+
target_url_base = path # 假设 path 已经包含了目标 URL 的基础部分
|
| 162 |
+
api_key = None
|
| 163 |
+
api_type = None
|
| 164 |
+
lookup_value = None
|
| 165 |
+
|
| 166 |
+
# 识别 API 类型并提取 lookup_value
|
| 167 |
+
if "open.bigmodel.cn" in target_url_base or "generativelanguage.googleapis.com" in target_url_base:
|
| 168 |
+
api_type = "llm"
|
| 169 |
+
try:
|
| 170 |
+
request_body = await request.json()
|
| 171 |
+
lookup_value = request_body.get("model")
|
| 172 |
+
if not lookup_value:
|
| 173 |
+
logger.warning(f"LLM request missing 'model' in body for path: {path}")
|
| 174 |
+
return create_error_response(400, "LLM request body must contain 'model' field.")
|
| 175 |
+
except json.JSONDecodeError:
|
| 176 |
+
logger.warning(f"LLM request body is not valid JSON for path: {path}")
|
| 177 |
+
return create_error_response(400, "LLM request body must be valid JSON.")
|
| 178 |
+
elif "api.tavily.com" in target_url_base:
|
| 179 |
+
api_type = "search"
|
| 180 |
+
lookup_value = "api.tavily.com" # 或者从 URL 中更动态地提取,例如使用正则表达式
|
| 181 |
+
else:
|
| 182 |
+
logger.warning(f"Unknown API type for path: {path}")
|
| 183 |
+
return create_error_response(400, "Unknown API type or unsupported target URL.")
|
| 184 |
+
|
| 185 |
+
# 从数据库获取 API 密钥
|
| 186 |
+
if api_type and lookup_value:
|
| 187 |
+
api_key_info = await get_api_key_info(api_type, lookup_value)
|
| 188 |
+
if api_key_info:
|
| 189 |
+
api_key = api_key_info["api_key"]
|
| 190 |
+
logger.info(f"Successfully retrieved API key for type: {api_type}, lookup_value: {lookup_value}")
|
| 191 |
+
else:
|
| 192 |
+
logger.warning(f"API Key not found for type: {api_type}, lookup_value: {lookup_value}")
|
| 193 |
+
return create_error_response(401, "API Key not found for the requested service.")
|
| 194 |
+
else:
|
| 195 |
+
logger.error(f"Failed to determine API type or lookup value for path: {path}")
|
| 196 |
+
return create_error_response(500, "Internal server error: Could not determine API key parameters.")
|
| 197 |
+
|
| 198 |
+
# 构建目标 URL
|
| 199 |
+
# 假设原始请求的 path 已经包含了完整的后端服务 URL,例如 /v1/https/open.bigmodel.cn/api/paas/v4/chat/completions
|
| 200 |
+
# 我们需要提取出 https://open.bigmodel.cn/api/paas/v4/chat/completions
|
| 201 |
+
match = re.match(r"/v1/(https?://.*)", path)
|
| 202 |
+
if not match:
|
| 203 |
+
logger.error(f"Invalid target URL format in path: {path}")
|
| 204 |
+
return create_error_response(400, "Invalid target URL format.")
|
| 205 |
+
|
| 206 |
+
full_target_url = match.group(1)
|
| 207 |
+
|
| 208 |
+
# 准备转发请求的头部和内容
|
| 209 |
+
headers = dict(request.headers)
|
| 210 |
+
headers.pop("host", None) # 移除 host 头,避免后端服务解析错误
|
| 211 |
+
headers.pop("authorization", None) # 移除原始的 authorization 头,我们将注入新的 API 密钥
|
| 212 |
+
|
| 213 |
+
# 注入 API 密钥
|
| 214 |
+
if api_type == "llm":
|
| 215 |
+
# 对于 LLM,通常 API 密钥在 Authorization 头中
|
| 216 |
+
headers["Authorization"] = f"Bearer {api_key}"
|
| 217 |
+
elif api_type == "search":
|
| 218 |
+
# 对于 Tavily 等搜索引擎,API 密钥可能在请求体中或自定义头中
|
| 219 |
+
# 这里假设 Tavily API 密钥在请求体中,需要修改请求体
|
| 220 |
+
# 注意:修改请求体需要重新构建请求,这可能比较复杂
|
| 221 |
+
# 更简单的做法是如果后端支持,通过自定义头传递
|
| 222 |
+
# 假设 Tavily API 密钥在请求体中,且请求体是 JSON
|
| 223 |
+
if request.method == "POST":
|
| 224 |
+
try:
|
| 225 |
+
request_body = await request.json()
|
| 226 |
+
request_body["api_key"] = api_key # 注入 Tavily API 密钥
|
| 227 |
+
request_content = json.dumps(request_body).encode("utf-8")
|
| 228 |
+
headers["Content-Type"] = "application/json"
|
| 229 |
+
except json.JSONDecodeError:
|
| 230 |
+
logger.error(f"Search request body is not valid JSON for path: {path}")
|
| 231 |
+
return create_error_response(400, "Search request body must be valid JSON.")
|
| 232 |
+
else:
|
| 233 |
+
# 如果是 GET 请求,API 密钥可能在查询参数中,这里需要根据实际情况调整
|
| 234 |
+
logger.warning(f"Search API key injection for GET request not implemented for path: {path}")
|
| 235 |
+
return create_error_response(501, "Search API key injection for GET requests is not yet supported.")
|
| 236 |
+
|
| 237 |
+
# 转发请求
|
| 238 |
+
try:
|
| 239 |
+
req = client.build_request(
|
| 240 |
+
method=request.method,
|
| 241 |
+
url=full_target_url,
|
| 242 |
+
headers=headers,
|
| 243 |
+
content=request_content if 'request_content' in locals() else await request.body(),
|
| 244 |
+
params=request.query_params,
|
| 245 |
+
timeout=30.0 # 设置超时时间
|
| 246 |
+
)
|
| 247 |
+
|
| 248 |
+
proxy_response = await client.send(req, stream=True)
|
| 249 |
+
|
| 250 |
+
return StreamingResponse(
|
| 251 |
+
proxy_response.aiter_bytes(),
|
| 252 |
+
status_code=proxy_response.status_code,
|
| 253 |
+
headers=proxy_response.headers
|
| 254 |
+
)
|
| 255 |
+
except httpx.RequestError as e:
|
| 256 |
+
logger.error(f"HTTPX Request Error for {full_target_url}: {e}")
|
| 257 |
+
return create_error_response(500, f"Proxy request failed: {e}")
|
| 258 |
+
except Exception as e:
|
| 259 |
+
logger.error(f"An unexpected error occurred: {e}")
|
| 260 |
+
return create_error_response(500, "An unexpected error occurred during proxying.")
|
proxy.py
CHANGED
|
@@ -39,6 +39,7 @@ async def do_proxy(url: str, method: str, headers: dict, content: str, max_retri
|
|
| 39 |
model_name = None
|
| 40 |
try:
|
| 41 |
content_json = json.loads(content)
|
|
|
|
| 42 |
model_name = content_json.get('model')
|
| 43 |
if model_name:
|
| 44 |
logger.info(f"从内容中提取的模型名称: {model_name}")
|
|
@@ -48,7 +49,7 @@ async def do_proxy(url: str, method: str, headers: dict, content: str, max_retri
|
|
| 48 |
logger.warning("内容不是有效的 JSON 格式,无法提取 'model' 键。")
|
| 49 |
except Exception as e:
|
| 50 |
logger.error(f"提取模型名称时发生错误: {str(e)}")
|
| 51 |
-
|
| 52 |
api_key_info = await get_api_key_info(model_name)
|
| 53 |
if not api_key_info:
|
| 54 |
return _create_error_response(status.HTTP_500_INTERNAL_SERVER_ERROR, "无法获取API密钥信息", "APIKeyError")
|
|
@@ -108,5 +109,6 @@ async def do_proxy(url: str, method: str, headers: dict, content: str, max_retri
|
|
| 108 |
return _create_error_response(e.status_code, e.detail, "HTTPException")
|
| 109 |
|
| 110 |
except Exception as e:
|
|
|
|
| 111 |
logger.error(f"🔥 致命错误: {type(e).__name__}: {str(e)}")
|
| 112 |
return _create_error_response(status.HTTP_500_INTERNAL_SERVER_ERROR, f"内部服务器错误: {str(e)}", "InternalServerError")
|
|
|
|
| 39 |
model_name = None
|
| 40 |
try:
|
| 41 |
content_json = json.loads(content)
|
| 42 |
+
|
| 43 |
model_name = content_json.get('model')
|
| 44 |
if model_name:
|
| 45 |
logger.info(f"从内容中提取的模型名称: {model_name}")
|
|
|
|
| 49 |
logger.warning("内容不是有效的 JSON 格式,无法提取 'model' 键。")
|
| 50 |
except Exception as e:
|
| 51 |
logger.error(f"提取模型名称时发生错误: {str(e)}")
|
| 52 |
+
# api_key_info = await get_api_key_info('chat', model_name)
|
| 53 |
api_key_info = await get_api_key_info(model_name)
|
| 54 |
if not api_key_info:
|
| 55 |
return _create_error_response(status.HTTP_500_INTERNAL_SERVER_ERROR, "无法获取API密钥信息", "APIKeyError")
|
|
|
|
| 109 |
return _create_error_response(e.status_code, e.detail, "HTTPException")
|
| 110 |
|
| 111 |
except Exception as e:
|
| 112 |
+
print('请检查当前是否运行在本机开发环境')
|
| 113 |
logger.error(f"🔥 致命错误: {type(e).__name__}: {str(e)}")
|
| 114 |
return _create_error_response(status.HTTP_500_INTERNAL_SERVER_ERROR, f"内部服务器错误: {str(e)}", "InternalServerError")
|