Compare commits

...

7 Commits

Author SHA1 Message Date
snaily
ebfa1d247c chore: 更新版本号至2.2.1 2025-07-20 13:43:18 +08:00
snaily
cdb85ef9b7 fix: 更新思考预算配置,使用_get_real_model函数获取真实模型 2025-07-20 13:42:36 +08:00
snaily
7006522c13 feat: 修复gemini-cli调用工具失败的问题 2025-07-20 13:19:59 +08:00
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
10 changed files with 176 additions and 17 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"] IMAGE_MODELS=["gemini-2.0-flash-exp"]
SEARCH_MODELS=["gemini-2.0-flash-exp","gemini-2.0-pro-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"] 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 TOOLS_CODE_EXECUTION_ENABLED=false
SHOW_SEARCH_LINK=true SHOW_SEARCH_LINK=true
SHOW_THINKING_PROCESS=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_MODELS` | Optional, list of models that support thinking functions | `[]` |
| `THINKING_BUDGET_MAP` | Optional, thinking function budget mapping (model_name:budget_value) | `{}` | | `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_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` | | `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_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` | | `MAX_RETRIES` | Optional, maximum number of retries for failed API requests | `3` |

View File

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

View File

@@ -1 +1 @@
2.1.13 2.2.1

View File

@@ -75,6 +75,9 @@ class Settings(BaseSettings):
IMAGE_MODELS: List[str] = ["gemini-2.0-flash-exp"] IMAGE_MODELS: List[str] = ["gemini-2.0-flash-exp"]
FILTERED_MODELS: List[str] = DEFAULT_FILTER_MODELS FILTERED_MODELS: List[str] = DEFAULT_FILTER_MODELS
TOOLS_CODE_EXECUTION_ENABLED: bool = False 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_SEARCH_LINK: bool = True
SHOW_THINKING_PROCESS: bool = True SHOW_THINKING_PROCESS: bool = True
THINKING_MODELS: List[str] = [] THINKING_MODELS: List[str] = []

View File

@@ -80,6 +80,21 @@ def _clean_json_schema_properties(obj: Any) -> Any:
def _build_tools(model: str, payload: Dict[str, Any]) -> List[Dict[str, Any]]: def _build_tools(model: str, payload: Dict[str, Any]) -> List[Dict[str, Any]]:
"""构建工具""" """构建工具"""
def _has_function_call(contents: List[Dict[str, Any]]) -> bool:
"""检查内容中是否包含 functionCall"""
if not contents or not isinstance(contents, list):
return False
for content in contents:
if not content or not isinstance(content, dict) or "parts" not in content:
continue
parts = content.get("parts", [])
if not parts or not isinstance(parts, list):
continue
for part in parts:
if isinstance(part, dict) and "functionCall" in part:
return True
return False
def _merge_tools(tools: List[Dict[str, Any]]) -> Dict[str, Any]: def _merge_tools(tools: List[Dict[str, Any]]) -> Dict[str, Any]:
record = dict() record = dict()
for item in tools: for item in tools:
@@ -119,15 +134,32 @@ def _build_tools(model: str, payload: Dict[str, Any]) -> List[Dict[str, Any]]:
tool["codeExecution"] = {} tool["codeExecution"] = {}
if model.endswith("-search"): if model.endswith("-search"):
tool["googleSearch"] = {} 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" 问题 # 解决 "Tool use with function calling is unsupported" 问题
if tool.get("functionDeclarations"): if tool.get("functionDeclarations") or _has_function_call(payload.get("contents", [])):
tool.pop("googleSearch", None) tool.pop("googleSearch", None)
tool.pop("codeExecution", None) tool.pop("codeExecution", None)
tool.pop("urlContext", None)
return [tool] if tool else [] 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]]: def _get_safety_settings(model: str) -> List[Dict[str, str]]:
"""获取安全设置""" """获取安全设置"""
if model == "gemini-2.0-flash-exp": if model == "gemini-2.0-flash-exp":
@@ -206,8 +238,11 @@ def _build_payload(model: str, request: GeminiRequest) -> Dict[str, Any]:
else: else:
# 客户端没有提供思考配置,使用默认配置 # 客户端没有提供思考配置,使用默认配置
if model.endswith("-non-thinking"): if model.endswith("-non-thinking"):
payload["generationConfig"]["thinkingConfig"] = {"thinkingBudget": 0} if "gemini-2.5-pro" in model:
elif model in settings.THINKING_BUDGET_MAP: payload["generationConfig"]["thinkingConfig"] = {"thinkingBudget": 128}
else:
payload["generationConfig"]["thinkingConfig"] = {"thinkingBudget": 0}
elif _get_real_model(model) in settings.THINKING_BUDGET_MAP:
if settings.SHOW_THINKING_PROCESS: if settings.SHOW_THINKING_PROCESS:
payload["generationConfig"]["thinkingConfig"] = { payload["generationConfig"]["thinkingConfig"] = {
"thinkingBudget": settings.THINKING_BUDGET_MAP.get(model,1000), "thinkingBudget": settings.THINKING_BUDGET_MAP.get(model,1000),

View File

@@ -87,6 +87,10 @@ def _build_tools(
if model.endswith("-search"): if model.endswith("-search"):
tool["googleSearch"] = {} 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 中 # 将 request 中的 tools 合并到 tools 中
if request.tools: if request.tools:
@@ -126,10 +130,23 @@ def _build_tools(
if tool.get("functionDeclarations"): if tool.get("functionDeclarations"):
tool.pop("googleSearch", None) tool.pop("googleSearch", None)
tool.pop("codeExecution", None) tool.pop("codeExecution", None)
tool.pop("urlContext",None)
return [tool] if tool else [] 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]]: def _get_safety_settings(model: str) -> List[Dict[str, str]]:
"""获取安全设置""" """获取安全设置"""
# if ( # if (
@@ -184,9 +201,12 @@ def _build_payload(
payload["generationConfig"]["responseModalities"] = ["Text", "Image"] payload["generationConfig"]["responseModalities"] = ["Text", "Image"]
if request.model.endswith("-non-thinking"): 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 _get_real_model(request.model) in settings.THINKING_BUDGET_MAP:
if settings.SHOW_THINKING_PROCESS: if settings.SHOW_THINKING_PROCESS:
payload["generationConfig"]["thinkingConfig"] = { payload["generationConfig"]["thinkingConfig"] = {
"thinkingBudget": settings.THINKING_BUDGET_MAP.get(request.model, 1000), "thinkingBudget": settings.THINKING_BUDGET_MAP.get(request.model, 1000),

View File

@@ -58,6 +58,21 @@ def _clean_json_schema_properties(obj: Any) -> Any:
def _build_tools(model: str, payload: Dict[str, Any]) -> List[Dict[str, Any]]: def _build_tools(model: str, payload: Dict[str, Any]) -> List[Dict[str, Any]]:
"""构建工具""" """构建工具"""
def _has_function_call(contents: List[Dict[str, Any]]) -> bool:
"""检查内容中是否包含 functionCall"""
if not contents or not isinstance(contents, list):
return False
for content in contents:
if not content or not isinstance(content, dict) or "parts" not in content:
continue
parts = content.get("parts", [])
if not parts or not isinstance(parts, list):
continue
for part in parts:
if isinstance(part, dict) and "functionCall" in part:
return True
return False
def _merge_tools(tools: List[Dict[str, Any]]) -> Dict[str, Any]: def _merge_tools(tools: List[Dict[str, Any]]) -> Dict[str, Any]:
record = dict() record = dict()
for item in tools: for item in tools:
@@ -97,15 +112,32 @@ def _build_tools(model: str, payload: Dict[str, Any]) -> List[Dict[str, Any]]:
tool["codeExecution"] = {} tool["codeExecution"] = {}
if model.endswith("-search"): if model.endswith("-search"):
tool["googleSearch"] = {} 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" 问题 # 解决 "Tool use with function calling is unsupported" 问题
if tool.get("functionDeclarations"): if tool.get("functionDeclarations") or _has_function_call(payload.get("contents", [])):
tool.pop("googleSearch", None) tool.pop("googleSearch", None)
tool.pop("codeExecution", None) tool.pop("codeExecution", None)
tool.pop("urlContext", None)
return [tool] if tool else [] 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]]: def _get_safety_settings(model: str) -> List[Dict[str, str]]:
"""获取安全设置""" """获取安全设置"""
if model == "gemini-2.0-flash-exp": if model == "gemini-2.0-flash-exp":
@@ -144,8 +176,11 @@ def _build_payload(model: str, request: GeminiRequest) -> Dict[str, Any]:
else: else:
# 客户端没有提供思考配置,使用默认配置 # 客户端没有提供思考配置,使用默认配置
if model.endswith("-non-thinking"): if model.endswith("-non-thinking"):
payload["generationConfig"]["thinkingConfig"] = {"thinkingBudget": 0} if "gemini-2.5-pro" in model:
elif model in settings.THINKING_BUDGET_MAP: payload["generationConfig"]["thinkingConfig"] = {"thinkingBudget": 128}
else:
payload["generationConfig"]["thinkingConfig"] = {"thinkingBudget": 0}
elif _get_real_model(model) in settings.THINKING_BUDGET_MAP:
if settings.SHOW_THINKING_PROCESS: if settings.SHOW_THINKING_PROCESS:
payload["generationConfig"]["thinkingConfig"] = { payload["generationConfig"]["thinkingConfig"] = {
"thinkingBudget": settings.THINKING_BUDGET_MAP.get(model,1000), "thinkingBudget": settings.THINKING_BUDGET_MAP.get(model,1000),

View File

@@ -13,7 +13,7 @@ const SHOW_CLASS = "show"; // For modals
const API_KEY_REGEX = /AIzaSy\S{33}/g; const API_KEY_REGEX = /AIzaSy\S{33}/g;
const PROXY_REGEX = const PROXY_REGEX =
/(?:https?|socks5):\/\/(?:[^:@\/]+(?::[^@\/]+)?@)?(?:[^:\/\s]+)(?::\d+)?/g; /(?: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 = "••••••••"; const MASKED_VALUE = "••••••••";
// DOM Elements - Global Scope for frequently accessed elements // DOM Elements - Global Scope for frequently accessed elements
@@ -713,7 +713,13 @@ async function initConfig() {
config.SAFETY_SETTINGS = []; // 默认为空数组 config.SAFETY_SETTINGS = []; // 默认为空数组
} }
// --- 结束:处理 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") { if (typeof config.AUTO_DELETE_ERROR_LOGS_ENABLED === "undefined") {
config.AUTO_DELETE_ERROR_LOGS_ENABLED = false; config.AUTO_DELETE_ERROR_LOGS_ENABLED = false;
@@ -1462,7 +1468,7 @@ function addArrayItem(key) {
const modelId = addArrayItemWithValue(key, newItemValue); // This adds the DOM element const modelId = addArrayItemWithValue(key, newItemValue); // This adds the DOM element
if (key === "THINKING_MODELS" && modelId) { 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"); const valueInput = document.createElement("input");
valueInput.type = "number"; valueInput.type = "number";
const intValue = parseInt(mapValue, 10); const intValue = parseInt(mapValue, 10);
valueInput.value = isNaN(intValue) ? 0 : intValue; valueInput.value = isNaN(intValue) ? -1 : intValue;
valueInput.placeholder = "预算 (整数)"; 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.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; valueInput.min = -1;
@@ -1733,7 +1739,7 @@ function collectFormData() {
formData["THINKING_BUDGET_MAP"][keyInput.value.trim()] = isNaN( formData["THINKING_BUDGET_MAP"][keyInput.value.trim()] = isNaN(
budgetValue budgetValue
) )
? 0 ? -1
: budgetValue; : budgetValue;
} }
}); });
@@ -2284,7 +2290,7 @@ function handleModelSelection(selectedModelId) {
); );
if (currentModelHelperTarget.targetKey === "THINKING_MODELS" && modelId) { if (currentModelHelperTarget.targetKey === "THINKING_MODELS" && modelId) {
// Automatically add corresponding budget map item with default budget 0 // 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> </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"> <div class="mb-6 flex items-center justify-between">
<label for="SHOW_SEARCH_LINK" class="font-semibold text-gray-700" <label for="SHOW_SEARCH_LINK" class="font-semibold text-gray-700"
@@ -1523,7 +1576,7 @@ endblock %} {% block head_extra_styles %}
</button> </button>
</div> --> </div> -->
<small class="text-gray-500 mt-1 block" <small class="text-gray-500 mt-1 block"
>为每个思考模型设置预算(整数,最大值 >为每个思考模型设置预算(-1为auto,最大值
32767此项与上方模型列表自动关联。</small 32767此项与上方模型列表自动关联。</small
> >
</div> </div>