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