feat: 添加自定义 Headers 功能

- 在配置中添加 `CUSTOM_HEADERS` 选项,允许用户定义全局请求头。
- 更新 API 客户端,将自定义 `header` 应用于所有出站请求。
- 在配置页面上为 `CUSTOM_HEADERS` 添加了完整的前端编辑功能。
This commit is contained in:
snaily
2025-07-08 13:58:05 +08:00
parent 0a08913677
commit 231b69ecf8
4 changed files with 147 additions and 10 deletions

View File

@@ -67,6 +67,9 @@ class Settings(BaseSettings):
# 智能路由配置
URL_NORMALIZATION_ENABLED: bool = False # 是否启用智能路由映射功能
# 自定义 Headers
CUSTOM_HEADERS: Dict[str, str] = {}
# 模型相关配置
SEARCH_MODELS: List[str] = ["gemini-2.0-flash-exp"]
IMAGE_MODELS: List[str] = ["gemini-2.0-flash-exp"]

View File

@@ -44,6 +44,13 @@ class GeminiApiClient(ApiClient):
model = model[:-20]
return model
def _prepare_headers(self) -> Dict[str, str]:
headers = {}
if settings.CUSTOM_HEADERS:
headers.update(settings.CUSTOM_HEADERS)
logger.info(f"Using custom headers: {settings.CUSTOM_HEADERS}")
return headers
async def get_models(self, api_key: str) -> Optional[Dict[str, Any]]:
"""获取可用的 Gemini 模型列表"""
timeout = httpx.Timeout(timeout=5)
@@ -56,10 +63,11 @@ class GeminiApiClient(ApiClient):
proxy_to_use = random.choice(settings.PROXIES)
logger.info(f"Using proxy for getting models: {proxy_to_use}")
headers = self._prepare_headers()
async with httpx.AsyncClient(timeout=timeout, proxy=proxy_to_use) as client:
url = f"{self.base_url}/models?key={api_key}&pageSize=1000"
try:
response = await client.get(url)
response = await client.get(url, headers=headers)
response.raise_for_status()
return response.json()
except httpx.HTTPStatusError as e:
@@ -82,9 +90,10 @@ class GeminiApiClient(ApiClient):
proxy_to_use = random.choice(settings.PROXIES)
logger.info(f"Using proxy for getting models: {proxy_to_use}")
headers = self._prepare_headers()
async with httpx.AsyncClient(timeout=timeout, proxy=proxy_to_use) as client:
url = f"{self.base_url}/models/{model}:generateContent?key={api_key}"
response = await client.post(url, json=payload)
response = await client.post(url, json=payload, headers=headers)
if response.status_code != 200:
error_content = response.text
raise Exception(f"API call failed with status code {response.status_code}, {error_content}")
@@ -102,9 +111,10 @@ class GeminiApiClient(ApiClient):
proxy_to_use = random.choice(settings.PROXIES)
logger.info(f"Using proxy for getting models: {proxy_to_use}")
headers = self._prepare_headers()
async with httpx.AsyncClient(timeout=timeout, proxy=proxy_to_use) as client:
url = f"{self.base_url}/models/{model}:streamGenerateContent?alt=sse&key={api_key}"
async with client.stream(method="POST", url=url, json=payload) as response:
async with client.stream(method="POST", url=url, json=payload, headers=headers) as response:
if response.status_code != 200:
error_content = await response.aread()
error_msg = error_content.decode("utf-8")
@@ -124,9 +134,10 @@ class GeminiApiClient(ApiClient):
proxy_to_use = random.choice(settings.PROXIES)
logger.info(f"Using proxy for counting tokens: {proxy_to_use}")
headers = self._prepare_headers()
async with httpx.AsyncClient(timeout=timeout, proxy=proxy_to_use) as client:
url = f"{self.base_url}/models/{model}:countTokens?key={api_key}"
response = await client.post(url, json=payload)
response = await client.post(url, json=payload, headers=headers)
if response.status_code != 200:
error_content = response.text
raise Exception(f"API call failed with status code {response.status_code}, {error_content}")
@@ -140,6 +151,13 @@ class OpenaiApiClient(ApiClient):
self.base_url = base_url
self.timeout = timeout
def _prepare_headers(self, api_key: str) -> Dict[str, str]:
headers = {"Authorization": f"Bearer {api_key}"}
if settings.CUSTOM_HEADERS:
headers.update(settings.CUSTOM_HEADERS)
logger.info(f"Using custom headers: {settings.CUSTOM_HEADERS}")
return headers
async def get_models(self, api_key: str) -> Dict[str, Any]:
timeout = httpx.Timeout(self.timeout, read=self.timeout)
@@ -151,9 +169,9 @@ class OpenaiApiClient(ApiClient):
proxy_to_use = random.choice(settings.PROXIES)
logger.info(f"Using proxy for getting models: {proxy_to_use}")
headers = self._prepare_headers(api_key)
async with httpx.AsyncClient(timeout=timeout, proxy=proxy_to_use) as client:
url = f"{self.base_url}/openai/models"
headers = {"Authorization": f"Bearer {api_key}"}
response = await client.get(url, headers=headers)
if response.status_code != 200:
error_content = response.text
@@ -171,9 +189,9 @@ class OpenaiApiClient(ApiClient):
proxy_to_use = random.choice(settings.PROXIES)
logger.info(f"Using proxy for getting models: {proxy_to_use}")
headers = self._prepare_headers(api_key)
async with httpx.AsyncClient(timeout=timeout, proxy=proxy_to_use) as client:
url = f"{self.base_url}/openai/chat/completions"
headers = {"Authorization": f"Bearer {api_key}"}
response = await client.post(url, json=payload, headers=headers)
if response.status_code != 200:
error_content = response.text
@@ -190,9 +208,9 @@ class OpenaiApiClient(ApiClient):
proxy_to_use = random.choice(settings.PROXIES)
logger.info(f"Using proxy for getting models: {proxy_to_use}")
headers = self._prepare_headers(api_key)
async with httpx.AsyncClient(timeout=timeout, proxy=proxy_to_use) as client:
url = f"{self.base_url}/openai/chat/completions"
headers = {"Authorization": f"Bearer {api_key}"}
async with client.stream(method="POST", url=url, json=payload, headers=headers) as response:
if response.status_code != 200:
error_content = await response.aread()
@@ -212,9 +230,9 @@ class OpenaiApiClient(ApiClient):
proxy_to_use = random.choice(settings.PROXIES)
logger.info(f"Using proxy for getting models: {proxy_to_use}")
headers = self._prepare_headers(api_key)
async with httpx.AsyncClient(timeout=timeout, proxy=proxy_to_use) as client:
url = f"{self.base_url}/openai/embeddings"
headers = {"Authorization": f"Bearer {api_key}"}
payload = {
"input": input,
"model": model,
@@ -236,9 +254,9 @@ class OpenaiApiClient(ApiClient):
proxy_to_use = random.choice(settings.PROXIES)
logger.info(f"Using proxy for getting models: {proxy_to_use}")
headers = self._prepare_headers(api_key)
async with httpx.AsyncClient(timeout=timeout, proxy=proxy_to_use) as client:
url = f"{self.base_url}/openai/images/generations"
headers = {"Authorization": f"Bearer {api_key}"}
response = await client.post(url, json=payload, headers=headers)
if response.status_code != 200:
error_content = response.text

View File

@@ -5,6 +5,9 @@ const ARRAY_INPUT_CLASS = "array-input";
const MAP_ITEM_CLASS = "map-item";
const MAP_KEY_INPUT_CLASS = "map-key-input";
const MAP_VALUE_INPUT_CLASS = "map-value-input";
const CUSTOM_HEADER_ITEM_CLASS = "custom-header-item";
const CUSTOM_HEADER_KEY_INPUT_CLASS = "custom-header-key-input";
const CUSTOM_HEADER_VALUE_INPUT_CLASS = "custom-header-value-input";
const SAFETY_SETTING_ITEM_CLASS = "safety-setting-item";
const SHOW_CLASS = "show"; // For modals
const API_KEY_REGEX = /AIzaSy\S{33}/g;
@@ -383,6 +386,12 @@ document.addEventListener("DOMContentLoaded", function () {
addSafetySettingBtn.addEventListener("click", () => addSafetySettingItem());
}
// Add Custom Header button
const addCustomHeaderBtn = document.getElementById("addCustomHeaderBtn");
if (addCustomHeaderBtn) {
addCustomHeaderBtn.addEventListener("click", () => addCustomHeaderItem());
}
initializeSensitiveFields(); // Initialize sensitive field handling
// Vertex API Key Modal Elements and Events
@@ -691,6 +700,14 @@ async function initConfig() {
) {
config.THINKING_BUDGET_MAP = {}; // 默认为空对象
}
// --- 新增:处理 CUSTOM_HEADERS 默认值 ---
if (
!config.CUSTOM_HEADERS ||
typeof config.CUSTOM_HEADERS !== "object" ||
config.CUSTOM_HEADERS === null
) {
config.CUSTOM_HEADERS = {}; // 默认为空对象
}
// --- 新增:处理 SAFETY_SETTINGS 默认值 ---
if (!config.SAFETY_SETTINGS || !Array.isArray(config.SAFETY_SETTINGS)) {
config.SAFETY_SETTINGS = []; // 默认为空数组
@@ -756,6 +773,7 @@ async function initConfig() {
VERTEX_EXPRESS_BASE_URL: "", // 确保默认值存在
THINKING_MODELS: [],
THINKING_BUDGET_MAP: {},
CUSTOM_HEADERS: {},
AUTO_DELETE_ERROR_LOGS_ENABLED: false,
AUTO_DELETE_ERROR_LOGS_DAYS: 7, // 新增默认值
AUTO_DELETE_REQUEST_LOGS_ENABLED: false, // 新增默认值
@@ -854,6 +872,20 @@ function populateForm(config) {
'<div class="text-gray-500 text-sm italic">请在上方添加思考模型,预算将自动关联。</div>';
}
// Populate CUSTOM_HEADERS
const customHeadersContainer = document.getElementById("CUSTOM_HEADERS_container");
let customHeadersAdded = false;
if (customHeadersContainer && config.CUSTOM_HEADERS && typeof config.CUSTOM_HEADERS === "object") {
for (const [key, value] of Object.entries(config.CUSTOM_HEADERS)) {
createAndAppendCustomHeaderItem(key, value);
customHeadersAdded = true;
}
}
if (!customHeadersAdded && customHeadersContainer) {
customHeadersContainer.innerHTML =
'<div class="text-gray-500 text-sm italic">添加自定义请求头,例如 X-Api-Key: your-key</div>';
}
// 4. Populate other array fields (excluding THINKING_MODELS)
for (const [key, value] of Object.entries(config)) {
if (Array.isArray(value) && key !== "THINKING_MODELS") {
@@ -1562,6 +1594,60 @@ function createAndAppendBudgetMapItem(mapKey, mapValue, modelId) {
container.appendChild(mapItem);
}
/**
* Adds a new custom header item to the DOM.
*/
function addCustomHeaderItem() {
createAndAppendCustomHeaderItem("", "");
}
/**
* Creates and appends a DOM element for a custom header.
* @param {string} key - The header key.
* @param {string} value - The header value.
*/
function createAndAppendCustomHeaderItem(key, value) {
const container = document.getElementById("CUSTOM_HEADERS_container");
if (!container) {
console.error("Cannot add custom header: CUSTOM_HEADERS_container not found!");
return;
}
const placeholder = container.querySelector(".text-gray-500.italic");
if (placeholder && container.children.length === 1 && container.firstChild === placeholder) {
container.innerHTML = "";
}
const headerItem = document.createElement("div");
headerItem.className = `${CUSTOM_HEADER_ITEM_CLASS} flex items-center mb-2 gap-2`;
const keyInput = document.createElement("input");
keyInput.type = "text";
keyInput.value = key;
keyInput.placeholder = "Header Name";
keyInput.className = `${CUSTOM_HEADER_KEY_INPUT_CLASS} flex-grow px-3 py-2 border border-gray-300 rounded-md focus:outline-none bg-gray-100 text-gray-500`;
const valueInput = document.createElement("input");
valueInput.type = "text";
valueInput.value = value;
valueInput.placeholder = "Header Value";
valueInput.className = `${CUSTOM_HEADER_VALUE_INPUT_CLASS} flex-grow px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50`;
const removeBtn = createRemoveButton();
removeBtn.addEventListener("click", () => {
headerItem.remove();
if (container.children.length === 0) {
container.innerHTML = '<div class="text-gray-500 text-sm italic">添加自定义请求头,例如 X-Api-Key: your-key</div>';
}
});
headerItem.appendChild(keyInput);
headerItem.appendChild(valueInput);
headerItem.appendChild(removeBtn);
container.appendChild(headerItem);
}
/**
* Collects all data from the configuration form.
* @returns {object} An object containing all configuration data.
@@ -1638,6 +1724,19 @@ function collectFormData() {
});
}
const customHeadersContainer = document.getElementById("CUSTOM_HEADERS_container");
if (customHeadersContainer) {
formData["CUSTOM_HEADERS"] = {};
const customHeaderItems = customHeadersContainer.querySelectorAll(`.${CUSTOM_HEADER_ITEM_CLASS}`);
customHeaderItems.forEach((item) => {
const keyInput = item.querySelector(`.${CUSTOM_HEADER_KEY_INPUT_CLASS}`);
const valueInput = item.querySelector(`.${CUSTOM_HEADER_VALUE_INPUT_CLASS}`);
if (keyInput && valueInput && keyInput.value.trim() !== "") {
formData["CUSTOM_HEADERS"][keyInput.value.trim()] = valueInput.value.trim();
}
});
}
if (safetySettingsContainer) {
formData["SAFETY_SETTINGS"] = [];
const settingItems = safetySettingsContainer.querySelectorAll(

View File

@@ -898,7 +898,24 @@ endblock %} {% block head_extra_styles %}
/>
<small class="text-gray-500 mt-1 block">Gemini API的基础URL</small>
</div>
<!-- 自定义Headers -->
<div class="mb-6">
<label for="CUSTOM_HEADERS" class="block font-semibold mb-2 text-gray-700">自定义Headers</label>
<div class="bg-white rounded-lg border border-gray-200 p-4 mb-2 space-y-3" id="CUSTOM_HEADERS_container" style="background-color: rgba(255, 255, 255, 0.95); border: 1px solid rgba(0, 0, 0, 0.12); color: #374151;">
<!-- 键值对将在这里动态添加 -->
<div class="text-gray-500 text-sm italic">
添加自定义请求头,例如 X-Api-Key: your-key
</div>
</div>
<div class="flex justify-end">
<button type="button" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg font-medium transition-all duration-200 flex items-center gap-2" id="addCustomHeaderBtn">
<i class="fas fa-plus"></i> 添加Header
</button>
</div>
<small class="text-gray-500 mt-1 block">在这里添加的键值对将被添加到所有出站API请求的Header中。</small>
</div>
<!-- Vertex API密钥列表 -->
<div class="mb-6">
<label for="VERTEX_API_KEYS" class="block font-semibold mb-2 text-gray-700"