Compare commits

...

11 Commits

Author SHA1 Message Date
snaily
530c958afc chore: 更新版本号至2.2.0 2025-07-20 01:47:35 +08:00
snaily
57d861b578 feat: 增加URL上下文理解功能
本次提交引入了一项新功能,允许模型在对话中理解所提供URL的上下文。

主要变更:

- **配置**:新增了 `URL_CONTEXT_ENABLED` 和 `URL_CONTEXT_MODELS` 两个配置项,用于控制此功能的开关和支持的模型列表。
- **后端服务**:在 `gemini_chat_service`、`openai_chat_service` 和 `vertex_express_chat_service` 中,为支持的模型动态添加 `urlContext` 工具。
- **前端界面**:在配置编辑器页面增加了相应的UI控件,方便用户启用/禁用该功能并管理支持的模型列表。
- **文档**:更新了 `.env.example`、`README.md` 和 `README_ZH.md`,包含了新配置项的说明。
2025-07-20 01:46:18 +08:00
snaily
99664298b9 fix: 更新思考配置,针对gemini-2.5-pro模型设置思考预算为128 2025-07-19 22:20:55 +08:00
snaily
a6fe5a7022 fix: 更新思考模型预算说明,使用-1表示自动预算 2025-07-19 22:11:36 +08:00
snaily
1918dad602 chore: 更新版本号至2.1.13 2025-07-19 15:24:36 +08:00
snaily
69399c291e fix: 在密钥验证成功时重置失败计数 2025-07-19 10:49:09 +08:00
snaily
9ec33ce320 fix: 为API_KEYS和ALLOWED_TOKENS添加默认值 2025-07-19 09:31:59 +08:00
snaily
c35d3aff7d chore: 更新版本号至2.1.12 2025-07-19 01:39:32 +08:00
snaily
2a5744d1c4 fix: 移除请求payload构建中的by_alias参数 2025-07-19 01:38:48 +08:00
snaily
825511506b chore: 更新版本号至2.1.11 2025-07-19 00:41:08 +08:00
snaily
5a98a701cb fix: 修复生成配置字段名称以符合API要求 2025-07-19 00:40:44 +08:00
12 changed files with 157 additions and 23 deletions

View File

@@ -20,6 +20,9 @@ THINKING_BUDGET_MAP={"gemini-2.5-flash-preview-04-17": 4000}
IMAGE_MODELS=["gemini-2.0-flash-exp"]
SEARCH_MODELS=["gemini-2.0-flash-exp","gemini-2.0-pro-exp"]
FILTERED_MODELS=["gemini-1.0-pro-vision-latest", "gemini-pro-vision", "chat-bison-001", "text-bison-001", "embedding-gecko-001"]
# 是否启用网址上下文,默认启用
URL_CONTEXT_ENABLED=true
URL_CONTEXT_MODELS=["gemini-2.5-pro","gemini-2.5-flash","gemini-2.5-flash-lite","gemini-2.0-flash","gemini-2.0-flash-live-001"]
TOOLS_CODE_EXECUTION_ENABLED=false
SHOW_SEARCH_LINK=true
SHOW_THINKING_PROCESS=true

View File

@@ -192,6 +192,8 @@ If you want to run the source code directly locally for development or testing,
| `THINKING_MODELS` | Optional, list of models that support thinking functions | `[]` |
| `THINKING_BUDGET_MAP` | Optional, thinking function budget mapping (model_name:budget_value) | `{}` |
| `URL_NORMALIZATION_ENABLED` | Optional, whether to enable intelligent URL routing mapping | `false` |
| `URL_CONTEXT_ENABLED` | Optional, whether to enable URL context understanding | `false` |
| `URL_CONTEXT_MODELS` | Optional, list of models that support URL context understanding | `[]` |
| `BASE_URL` | Optional, Gemini API base URL, no modification needed by default | `https://generativelanguage.googleapis.com/v1beta` |
| `MAX_FAILURES` | Optional, number of times a single key is allowed to fail | `3` |
| `MAX_RETRIES` | Optional, maximum number of retries for failed API requests | `3` |

View File

@@ -185,6 +185,8 @@ app/
| `THINKING_MODELS` | 可选,支持思考功能的模型列表 | `[]` |
| `THINKING_BUDGET_MAP` | 可选,思考功能预算映射 (模型名:预算值) | `{}` |
| `URL_NORMALIZATION_ENABLED` | 可选,是否启用智能路由映射功能 | `false` |
| `URL_CONTEXT_ENABLED` | 可选是否启用URL上下文理解功能 | `false` |
| `URL_CONTEXT_MODELS` | 可选支持URL上下文理解功能的模型列表 | `[]` |
| `BASE_URL` | 可选Gemini API 基础 URL默认无需修改 | `https://generativelanguage.googleapis.com/v1beta` |
| `MAX_FAILURES` | 可选允许单个key失败的次数 | `3` |
| `MAX_RETRIES` | 可选API 请求失败时的最大重试次数 | `3` |

View File

@@ -1 +1 @@
2.1.10
2.2.0

View File

@@ -51,8 +51,8 @@ class Settings(BaseSettings):
return v
# API相关配置
API_KEYS: List[str]
ALLOWED_TOKENS: List[str]
API_KEYS: List[str]=[]
ALLOWED_TOKENS: List[str]=[]
BASE_URL: str = f"https://generativelanguage.googleapis.com/{API_VERSION}"
AUTH_TOKEN: str = ""
MAX_FAILURES: int = 3
@@ -75,6 +75,9 @@ class Settings(BaseSettings):
IMAGE_MODELS: List[str] = ["gemini-2.0-flash-exp"]
FILTERED_MODELS: List[str] = DEFAULT_FILTER_MODELS
TOOLS_CODE_EXECUTION_ENABLED: bool = False
# 是否启用网址上下文
URL_CONTEXT_ENABLED: bool = True
URL_CONTEXT_MODELS: List[str] = ["gemini-2.5-pro","gemini-2.5-flash","gemini-2.5-flash-lite","gemini-2.0-flash","gemini-2.0-flash-live-001"]
SHOW_SEARCH_LINK: bool = True
SHOW_THINKING_PROCESS: bool = True
THINKING_MODELS: List[str] = []

View File

@@ -329,7 +329,7 @@ async def verify_key(api_key: str, chat_service: GeminiChatService = Depends(get
parts=[{"text": "hi"}],
)
],
generation_config={"temperature": 0.7, "top_p": 1.0, "max_output_tokens": 10}
generation_config={"temperature": 0.7, "topP": 1.0, "maxOutputTokens": 10}
)
response = await chat_service.generate_content(
@@ -339,7 +339,9 @@ async def verify_key(api_key: str, chat_service: GeminiChatService = Depends(get
)
if response:
return JSONResponse({"status": "valid"})
# 如果密钥验证成功,则重置其失败计数
await key_manager.reset_key_failure_count(api_key)
return JSONResponse({"status": "valid"})
except Exception as e:
logger.error(f"Key verification failed: {str(e)}")
@@ -374,7 +376,7 @@ async def verify_selected_keys(
try:
gemini_request = GeminiRequest(
contents=[GeminiContent(role="user", parts=[{"text": "hi"}])],
generation_config={"temperature": 0.7, "top_p": 1.0, "max_output_tokens": 10}
generation_config={"temperature": 0.7, "topP": 1.0, "maxOutputTokens": 10}
)
await chat_service.generate_content(
settings.TEST_MODEL,
@@ -382,6 +384,8 @@ async def verify_selected_keys(
api_key
)
successful_keys.append(api_key)
# 如果密钥验证成功,则重置其失败计数
await key_manager.reset_key_failure_count(api_key)
return api_key, "valid", None
except Exception as e:
error_message = str(e)

View File

@@ -119,15 +119,32 @@ def _build_tools(model: str, payload: Dict[str, Any]) -> List[Dict[str, Any]]:
tool["codeExecution"] = {}
if model.endswith("-search"):
tool["googleSearch"] = {}
real_model = _get_real_model(model)
if real_model in settings.URL_CONTEXT_MODELS and settings.URL_CONTEXT_ENABLED:
tool["urlContext"] = {}
# 解决 "Tool use with function calling is unsupported" 问题
if tool.get("functionDeclarations"):
tool.pop("googleSearch", None)
tool.pop("codeExecution", None)
tool.pop("urlContext", None)
return [tool] if tool else []
def _get_real_model(model: str) -> str:
if model.endswith("-search"):
model = model[:-7]
if model.endswith("-image"):
model = model[:-6]
if model.endswith("-non-thinking"):
model = model[:-13]
if "-search" in model and "-non-thinking" in model:
model = model[:-20]
return model
def _get_safety_settings(model: str) -> List[Dict[str, str]]:
"""获取安全设置"""
if model == "gemini-2.0-flash-exp":
@@ -157,11 +174,12 @@ def _filter_empty_parts(contents: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
def _build_payload(model: str, request: GeminiRequest) -> Dict[str, Any]:
"""构建请求payload"""
request_dict = request.model_dump(by_alias=True, exclude_none=False)
request_dict = request.model_dump(exclude_none=False)
if request.generationConfig:
if request.generationConfig.maxOutputTokens is None:
# 如果未指定最大输出长度,则不传递该字段,解决截断的问题
request_dict["generationConfig"].pop("maxOutputTokens")
if "maxOutputTokens" in request_dict["generationConfig"]:
request_dict["generationConfig"].pop("maxOutputTokens")
# 检查是否为TTS模型
is_tts_model = "tts" in model.lower()
@@ -205,7 +223,10 @@ def _build_payload(model: str, request: GeminiRequest) -> Dict[str, Any]:
else:
# 客户端没有提供思考配置,使用默认配置
if model.endswith("-non-thinking"):
payload["generationConfig"]["thinkingConfig"] = {"thinkingBudget": 0}
if "gemini-2.5-pro" in model:
payload["generationConfig"]["thinkingConfig"] = {"thinkingBudget": 128}
else:
payload["generationConfig"]["thinkingConfig"] = {"thinkingBudget": 0}
elif model in settings.THINKING_BUDGET_MAP:
if settings.SHOW_THINKING_PROCESS:
payload["generationConfig"]["thinkingConfig"] = {

View File

@@ -87,6 +87,10 @@ def _build_tools(
if model.endswith("-search"):
tool["googleSearch"] = {}
real_model = _get_real_model(model)
if real_model in settings.URL_CONTEXT_MODELS and settings.URL_CONTEXT_ENABLED:
tool["urlContext"] = {}
# 将 request 中的 tools 合并到 tools 中
if request.tools:
@@ -126,10 +130,23 @@ def _build_tools(
if tool.get("functionDeclarations"):
tool.pop("googleSearch", None)
tool.pop("codeExecution", None)
tool.pop("urlContext",None)
return [tool] if tool else []
def _get_real_model(model: str) -> str:
if model.endswith("-search"):
model = model[:-7]
if model.endswith("-image"):
model = model[:-6]
if model.endswith("-non-thinking"):
model = model[:-13]
if "-search" in model and "-non-thinking" in model:
model = model[:-20]
return model
def _get_safety_settings(model: str) -> List[Dict[str, str]]:
"""获取安全设置"""
# if (
@@ -184,7 +201,10 @@ def _build_payload(
payload["generationConfig"]["responseModalities"] = ["Text", "Image"]
if request.model.endswith("-non-thinking"):
payload["generationConfig"]["thinkingConfig"] = {"thinkingBudget": 0}
if "gemini-2.5-pro" in request.model:
payload["generationConfig"]["thinkingConfig"] = {"thinkingBudget": 128}
else:
payload["generationConfig"]["thinkingConfig"] = {"thinkingBudget": 0}
if request.model in settings.THINKING_BUDGET_MAP:
if settings.SHOW_THINKING_PROCESS:

View File

@@ -97,15 +97,32 @@ def _build_tools(model: str, payload: Dict[str, Any]) -> List[Dict[str, Any]]:
tool["codeExecution"] = {}
if model.endswith("-search"):
tool["googleSearch"] = {}
real_model = _get_real_model(model)
if real_model in settings.URL_CONTEXT_MODELS and settings.URL_CONTEXT_ENABLED:
tool["urlContext"] = {}
# 解决 "Tool use with function calling is unsupported" 问题
if tool.get("functionDeclarations"):
tool.pop("googleSearch", None)
tool.pop("codeExecution", None)
tool.pop("urlContext", None)
return [tool] if tool else []
def _get_real_model(model: str) -> str:
if model.endswith("-search"):
model = model[:-7]
if model.endswith("-image"):
model = model[:-6]
if model.endswith("-non-thinking"):
model = model[:-13]
if "-search" in model and "-non-thinking" in model:
model = model[:-20]
return model
def _get_safety_settings(model: str) -> List[Dict[str, str]]:
"""获取安全设置"""
if model == "gemini-2.0-flash-exp":
@@ -115,7 +132,7 @@ def _get_safety_settings(model: str) -> List[Dict[str, str]]:
def _build_payload(model: str, request: GeminiRequest) -> Dict[str, Any]:
"""构建请求payload"""
request_dict = request.model_dump(by_alias=True, exclude_none=False)
request_dict = request.model_dump(exclude_none=False)
if request.generationConfig:
if request.generationConfig.maxOutputTokens is None:
# 如果未指定最大输出长度,则不传递该字段,解决截断的问题
@@ -144,7 +161,10 @@ def _build_payload(model: str, request: GeminiRequest) -> Dict[str, Any]:
else:
# 客户端没有提供思考配置,使用默认配置
if model.endswith("-non-thinking"):
payload["generationConfig"]["thinkingConfig"] = {"thinkingBudget": 0}
if "gemini-2.5-pro" in model:
payload["generationConfig"]["thinkingConfig"] = {"thinkingBudget": 128}
else:
payload["generationConfig"]["thinkingConfig"] = {"thinkingBudget": 0}
elif model in settings.THINKING_BUDGET_MAP:
if settings.SHOW_THINKING_PROCESS:
payload["generationConfig"]["thinkingConfig"] = {

View File

@@ -5,7 +5,7 @@
import time
import datetime
from typing import Any, Dict, Optional
from typing import Any, Dict
from app.service.chat.gemini_chat_service import GeminiChatService
from app.service.tts.native.tts_response_handler import TTSResponseHandler
from app.domain.gemini_models import GeminiRequest
@@ -69,7 +69,7 @@ class TTSGeminiChatService(GeminiChatService):
# 构建TTS专用的payload - 不包含tools和safetySettings
from app.service.chat.gemini_chat_service import _filter_empty_parts
request_dict = request.model_dump(by_alias=True, exclude_none=False)
request_dict = request.model_dump(exclude_none=False)
# 构建TTS专用的简化payload
payload = {
@@ -130,7 +130,7 @@ class TTSGeminiChatService(GeminiChatService):
error_type="tts-api-error",
error_log=error_msg,
error_code=status_code,
request_msg=request.model_dump(by_alias=True, exclude_none=False)
request_msg=request.model_dump(exclude_none=False)
)
logger.error(f"TTS API call failed: {error_msg}")

View File

@@ -13,7 +13,7 @@ const SHOW_CLASS = "show"; // For modals
const API_KEY_REGEX = /AIzaSy\S{33}/g;
const PROXY_REGEX =
/(?:https?|socks5):\/\/(?:[^:@\/]+(?::[^@\/]+)?@)?(?:[^:\/\s]+)(?::\d+)?/g;
const VERTEX_API_KEY_REGEX = /AQ\.[a-zA-Z0-9_]{50}/g; // 新增 Vertex Express API Key 正则
const VERTEX_API_KEY_REGEX = /AQ\.[a-zA-Z0-9_\-]{50}/g; // 新增 Vertex Express API Key 正则
const MASKED_VALUE = "••••••••";
// DOM Elements - Global Scope for frequently accessed elements
@@ -713,7 +713,13 @@ async function initConfig() {
config.SAFETY_SETTINGS = []; // 默认为空数组
}
// --- 结束:处理 SAFETY_SETTINGS 默认值 ---
if (typeof config.URL_CONTEXT_ENABLED === "undefined") {
config.URL_CONTEXT_ENABLED = true;
}
if (!config.URL_CONTEXT_MODELS || !Array.isArray(config.URL_CONTEXT_MODELS)) {
config.URL_CONTEXT_MODELS = [];
}
// --- 新增:处理自动删除错误日志配置的默认值 ---
if (typeof config.AUTO_DELETE_ERROR_LOGS_ENABLED === "undefined") {
config.AUTO_DELETE_ERROR_LOGS_ENABLED = false;
@@ -1462,7 +1468,7 @@ function addArrayItem(key) {
const modelId = addArrayItemWithValue(key, newItemValue); // This adds the DOM element
if (key === "THINKING_MODELS" && modelId) {
createAndAppendBudgetMapItem(newItemValue, 0, modelId); // Default budget 0
createAndAppendBudgetMapItem(newItemValue, -1, modelId); // Default budget -1
}
}
@@ -1572,7 +1578,7 @@ function createAndAppendBudgetMapItem(mapKey, mapValue, modelId) {
const valueInput = document.createElement("input");
valueInput.type = "number";
const intValue = parseInt(mapValue, 10);
valueInput.value = isNaN(intValue) ? 0 : intValue;
valueInput.value = isNaN(intValue) ? -1 : intValue;
valueInput.placeholder = "预算 (整数)";
valueInput.className = `${MAP_VALUE_INPUT_CLASS} w-24 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`;
valueInput.min = -1;
@@ -1733,7 +1739,7 @@ function collectFormData() {
formData["THINKING_BUDGET_MAP"][keyInput.value.trim()] = isNaN(
budgetValue
)
? 0
? -1
: budgetValue;
}
});
@@ -2284,7 +2290,7 @@ function handleModelSelection(selectedModelId) {
);
if (currentModelHelperTarget.targetKey === "THINKING_MODELS" && modelId) {
// Automatically add corresponding budget map item with default budget 0
createAndAppendBudgetMapItem(selectedModelId, 0, modelId);
createAndAppendBudgetMapItem(selectedModelId, -1, modelId);
}
}

View File

@@ -1421,6 +1421,59 @@ endblock %} {% block head_extra_styles %}
</div>
</div>
<!-- 启用网址上下文 -->
<div class="mb-6 flex items-center justify-between">
<label for="URL_CONTEXT_ENABLED" class="font-semibold text-gray-700"
>启用网址上下文</label
>
<div
class="relative inline-block w-10 mr-2 align-middle select-none transition duration-200 ease-in"
>
<input
type="checkbox"
name="URL_CONTEXT_ENABLED"
id="URL_CONTEXT_ENABLED"
class="toggle-checkbox absolute block w-6 h-6 rounded-full bg-white border-4 appearance-none cursor-pointer"
/>
<label
for="URL_CONTEXT_ENABLED"
class="toggle-label block overflow-hidden h-6 rounded-full bg-gray-300 cursor-pointer"
></label>
</div>
</div>
<!-- 网址上下文模型列表 -->
<div class="mb-6">
<label
for="URL_CONTEXT_MODELS"
class="block font-semibold mb-2 text-gray-700"
>网址上下文模型列表</label
>
<div class="array-container" id="URL_CONTEXT_MODELS_container">
<!-- 数组项将在这里动态添加 -->
</div>
<div class="flex justify-end gap-2 mt-2">
<button
type="button"
title="从列表选择模型添加到下方"
class="model-helper-trigger-btn p-2 rounded-md text-violet-300 hover:bg-violet-700 transition-colors"
data-target-array-key="URL_CONTEXT_MODELS"
>
<i class="fas fa-list-ul"></i>
</button>
<button
type="button"
class="bg-violet-600 hover:bg-violet-700 text-white px-4 py-2 rounded-lg font-medium transition-all duration-200 flex items-center gap-2"
onclick="addArrayItem('URL_CONTEXT_MODELS')"
>
<i class="fas fa-plus"></i> 添加模型
</button>
</div>
<small class="text-gray-500 mt-1 block"
>支持网址上下文功能的模型列表</small
>
</div>
<!-- 显示搜索链接 -->
<div class="mb-6 flex items-center justify-between">
<label for="SHOW_SEARCH_LINK" class="font-semibold text-gray-700"
@@ -1523,7 +1576,7 @@ endblock %} {% block head_extra_styles %}
</button>
</div> -->
<small class="text-gray-500 mt-1 block"
>为每个思考模型设置预算(整数,最大值
>为每个思考模型设置预算(-1为auto,最大值
32767此项与上方模型列表自动关联。</small
>
</div>