feat: 更新 4.20 模型与模型池拖拽路由
This commit is contained in:
1 parent
7f4b1dcd75
commit
83ae47d61c
7 files changed
+510
-19
No files matched your search
@@ -4,6 +4,8 @@ from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
from app.core.auth import verify_app_key
|
||||
from app.core.config import config
|
||||
from app.services.grok.services.model import ModelService
|
||||
from app.services.token import get_token_manager
|
||||
from app.core.storage import (
|
||||
get_storage as get_storage_backend,
|
||||
LocalStorage,
|
||||
@@ -37,6 +39,27 @@ async def update_config(data: dict):
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/model-routing/meta", dependencies=[Depends(verify_app_key)])
|
||||
async def get_model_routing_meta():
|
||||
"""获取模型池路由界面所需的模型与池元数据。"""
|
||||
token_mgr = await get_token_manager()
|
||||
pool_names = set(token_mgr.pools.keys())
|
||||
pool_names.update({"ssoBasic", "ssoSuper"})
|
||||
|
||||
models = [
|
||||
{
|
||||
"id": item.model_id,
|
||||
"display_name": item.display_name,
|
||||
}
|
||||
for item in ModelService.list()
|
||||
]
|
||||
|
||||
return {
|
||||
"models": models,
|
||||
"pools": sorted(pool_names),
|
||||
}
|
||||
|
||||
|
||||
@router.get("/storage", dependencies=[Depends(verify_app_key)])
|
||||
async def admin_get_storage():
|
||||
"""获取当前存储模式"""
|
||||
|
||||
@@ -6,6 +6,7 @@ from enum import Enum
|
||||
from typing import Optional, Tuple, List
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.core.config import get_config
|
||||
from app.core.exceptions import ValidationException
|
||||
|
||||
|
||||
@@ -36,6 +37,7 @@ class ModelInfo(BaseModel):
|
||||
is_image: bool = False
|
||||
is_image_edit: bool = False
|
||||
is_video: bool = False
|
||||
public: bool = True
|
||||
|
||||
|
||||
class ModelService:
|
||||
@@ -164,16 +166,50 @@ class ModelService:
|
||||
is_video=False,
|
||||
),
|
||||
ModelInfo(
|
||||
model_id="grok-4.20-beta",
|
||||
model_id="grok-4.20-auto",
|
||||
grok_model="grok-420",
|
||||
model_mode="MODEL_MODE_GROK_420",
|
||||
model_mode="auto",
|
||||
tier=Tier.BASIC,
|
||||
cost=Cost.LOW,
|
||||
display_name="GROK-4.20-BETA",
|
||||
display_name="GROK-4.20-AUTO",
|
||||
is_image=False,
|
||||
is_image_edit=False,
|
||||
is_video=False,
|
||||
),
|
||||
ModelInfo(
|
||||
model_id="grok-4.20-fast",
|
||||
grok_model="grok-420",
|
||||
model_mode="fast",
|
||||
tier=Tier.BASIC,
|
||||
cost=Cost.LOW,
|
||||
display_name="GROK-4.20-FAST",
|
||||
is_image=False,
|
||||
is_image_edit=False,
|
||||
is_video=False,
|
||||
),
|
||||
ModelInfo(
|
||||
model_id="grok-4.20-expert",
|
||||
grok_model="grok-420",
|
||||
model_mode="expert",
|
||||
tier=Tier.BASIC,
|
||||
cost=Cost.HIGH,
|
||||
display_name="GROK-4.20-EXPERT",
|
||||
is_image=False,
|
||||
is_image_edit=False,
|
||||
is_video=False,
|
||||
),
|
||||
ModelInfo(
|
||||
model_id="grok-4.20-beta",
|
||||
grok_model="grok-420",
|
||||
model_mode="auto",
|
||||
tier=Tier.BASIC,
|
||||
cost=Cost.LOW,
|
||||
display_name="GROK-4.20-AUTO",
|
||||
is_image=False,
|
||||
is_image_edit=False,
|
||||
is_video=False,
|
||||
public=False,
|
||||
),
|
||||
ModelInfo(
|
||||
model_id="grok-imagine-1.0",
|
||||
grok_model="grok-3",
|
||||
@@ -214,6 +250,46 @@ class ModelService:
|
||||
|
||||
_map = {m.model_id: m for m in MODELS}
|
||||
|
||||
@classmethod
|
||||
def _default_pool_candidates(cls, model_id: str) -> List[str]:
|
||||
"""返回模型默认池顺序。"""
|
||||
model = cls.get(model_id)
|
||||
if model and model.tier == Tier.SUPER:
|
||||
return ["ssoSuper"]
|
||||
# 基础模型优先使用 basic 池,缺失时可回退到 super 池
|
||||
return ["ssoBasic", "ssoSuper"]
|
||||
|
||||
@classmethod
|
||||
def _routing_lookup_keys(cls, model_id: str) -> List[str]:
|
||||
"""返回模型池路由查找顺序,兼容历史别名。"""
|
||||
keys = [model_id]
|
||||
if model_id == "grok-4.20-beta":
|
||||
keys.append("grok-4.20-auto")
|
||||
return keys
|
||||
|
||||
@classmethod
|
||||
def _configured_pool_candidates(cls, model_id: str) -> Optional[List[str]]:
|
||||
"""读取管理配置中的模型池路由。"""
|
||||
routing = get_config("model_routing.model_pools", {})
|
||||
if not isinstance(routing, dict):
|
||||
return None
|
||||
|
||||
for key in cls._routing_lookup_keys(model_id):
|
||||
value = routing.get(key)
|
||||
if isinstance(value, str):
|
||||
pool = value.strip()
|
||||
if pool:
|
||||
return [pool]
|
||||
elif isinstance(value, list):
|
||||
pools = [
|
||||
str(item).strip()
|
||||
for item in value
|
||||
if str(item).strip()
|
||||
]
|
||||
if pools:
|
||||
return pools
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def get(cls, model_id: str) -> Optional[ModelInfo]:
|
||||
"""获取模型信息"""
|
||||
@@ -222,7 +298,7 @@ class ModelService:
|
||||
@classmethod
|
||||
def list(cls) -> list[ModelInfo]:
|
||||
"""获取所有模型"""
|
||||
return list(cls._map.values())
|
||||
return [model for model in cls._map.values() if model.public]
|
||||
|
||||
@classmethod
|
||||
def valid(cls, model_id: str) -> bool:
|
||||
@@ -240,19 +316,16 @@ class ModelService:
|
||||
@classmethod
|
||||
def pool_for_model(cls, model_id: str) -> str:
|
||||
"""根据模型选择 Token 池"""
|
||||
model = cls.get(model_id)
|
||||
if model and model.tier == Tier.SUPER:
|
||||
return "ssoSuper"
|
||||
return "ssoBasic"
|
||||
pools = cls.pool_candidates_for_model(model_id)
|
||||
return pools[0] if pools else "ssoBasic"
|
||||
|
||||
@classmethod
|
||||
def pool_candidates_for_model(cls, model_id: str) -> List[str]:
|
||||
"""按优先级返回可用 Token 池列表"""
|
||||
model = cls.get(model_id)
|
||||
if model and model.tier == Tier.SUPER:
|
||||
return ["ssoSuper"]
|
||||
# 基础模型优先使用 basic 池,缺失时可回退到 super 池
|
||||
return ["ssoBasic", "ssoSuper"]
|
||||
configured = cls._configured_pool_candidates(model_id)
|
||||
if configured:
|
||||
return configured
|
||||
return cls._default_pool_candidates(model_id)
|
||||
|
||||
|
||||
__all__ = ["ModelService"]
|
||||
@@ -91,7 +91,6 @@ class AppChatReverse:
|
||||
"isAsyncChat": False,
|
||||
"isReasoning": False,
|
||||
"message": message,
|
||||
"modelMode": mode,
|
||||
"modelName": model,
|
||||
"responseMetadata": {
|
||||
"requestModelDetails": {"modelId": model},
|
||||
@@ -108,6 +107,10 @@ class AppChatReverse:
|
||||
|
||||
if model == "grok-420":
|
||||
payload["enable420"] = True
|
||||
if mode:
|
||||
payload["modeId"] = mode
|
||||
elif mode:
|
||||
payload["modelMode"] = mode
|
||||
|
||||
custom_personality = AppChatReverse._resolve_custom_personality()
|
||||
if custom_personality is not None and "Greet the user" not in message[-1000:]:
|
||||
|
||||
@@ -58,6 +58,169 @@
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.routing-board {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.routing-pane {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 14px;
|
||||
padding: 14px;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, rgba(249, 250, 251, 0.96) 100%);
|
||||
}
|
||||
|
||||
.routing-pane-header {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.routing-pane-title {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: var(--accents-7);
|
||||
}
|
||||
|
||||
.routing-pane-subtitle {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: var(--accents-4);
|
||||
}
|
||||
|
||||
.routing-dropzone {
|
||||
min-height: 148px;
|
||||
border: 1px dashed var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-content: flex-start;
|
||||
gap: 10px;
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(0, 0, 0, 0.03), transparent 42%),
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(244, 244, 245, 0.96));
|
||||
transition: border-color 0.2s ease, background-color 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.routing-dropzone.is-over {
|
||||
border-color: #111;
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(0, 0, 0, 0.06), transparent 42%),
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(237, 237, 239, 0.98));
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.routing-dropzone-source {
|
||||
min-height: 320px;
|
||||
}
|
||||
|
||||
.routing-pool-grid {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.routing-pool-card {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
background: rgba(255, 255, 255, 0.86);
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.routing-pool-header {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.routing-pool-title {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: var(--accents-7);
|
||||
}
|
||||
|
||||
.routing-pool-hint {
|
||||
font-size: 11px;
|
||||
color: var(--accents-4);
|
||||
}
|
||||
|
||||
.routing-chip {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
max-width: 100%;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid rgba(17, 17, 17, 0.12);
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(180deg, #111 0%, #262626 100%);
|
||||
color: #fff;
|
||||
cursor: grab;
|
||||
box-shadow: 0 6px 18px rgba(17, 17, 17, 0.1);
|
||||
transition: transform 0.15s ease, box-shadow 0.15s ease, opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.routing-chip:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 10px 24px rgba(17, 17, 17, 0.14);
|
||||
}
|
||||
|
||||
.routing-chip.is-dragging {
|
||||
opacity: 0.5;
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.routing-chip-name {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.routing-chip-id {
|
||||
max-width: 100%;
|
||||
font-size: 11px;
|
||||
color: rgba(255, 255, 255, 0.72);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.routing-empty {
|
||||
width: 100%;
|
||||
min-height: 72px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
padding: 12px;
|
||||
font-size: 12px;
|
||||
color: var(--accents-4);
|
||||
background: rgba(245, 245, 245, 0.9);
|
||||
}
|
||||
|
||||
html[data-theme='dark'] .routing-pane,
|
||||
html[data-theme='dark'] .routing-pool-card {
|
||||
background: linear-gradient(180deg, rgba(29, 29, 31, 0.96), rgba(20, 20, 22, 0.96));
|
||||
}
|
||||
|
||||
html[data-theme='dark'] .routing-dropzone {
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(255, 255, 255, 0.04), transparent 42%),
|
||||
linear-gradient(180deg, rgba(24, 24, 27, 0.98), rgba(16, 16, 18, 0.98));
|
||||
}
|
||||
|
||||
html[data-theme='dark'] .routing-dropzone.is-over {
|
||||
border-color: rgba(255, 255, 255, 0.76);
|
||||
}
|
||||
|
||||
html[data-theme='dark'] .routing-empty {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.config-section {
|
||||
padding: 14px;
|
||||
@@ -75,4 +238,12 @@
|
||||
bottom: 0;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.routing-board {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.routing-dropzone-source {
|
||||
min-height: 220px;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
let apiKey = '';
|
||||
let currentConfig = {};
|
||||
let modelRoutingMeta = { models: [], pools: ['ssoBasic', 'ssoSuper'] };
|
||||
let modelRoutingAssignments = {};
|
||||
let modelRoutingDragId = '';
|
||||
const byId = (id) => document.getElementById(id);
|
||||
const NUMERIC_FIELDS = new Set([
|
||||
'timeout',
|
||||
@@ -139,6 +142,12 @@ const LOCALE_MAP = {
|
||||
},
|
||||
|
||||
|
||||
"model_routing": {
|
||||
"label": "模型池路由",
|
||||
"model_pools": { title: "模型到 Token 池映射", desc: "为每个模型手动指定使用哪个 Token 池。值支持字符串或字符串数组,例如 {\"grok-4.20-expert\":\"ssoSuper\",\"grok-4.20-auto\":[\"ssoBasic\",\"ssoSuper\"]}。" }
|
||||
},
|
||||
|
||||
|
||||
"cache": {
|
||||
"label": "缓存管理",
|
||||
"enable_auto_clean": { title: "自动清理", desc: "是否启用缓存自动清理,开启后按上限自动回收。" },
|
||||
@@ -164,7 +173,8 @@ const LOCALE_MAP = {
|
||||
|
||||
// 配置部分说明(可选)
|
||||
const SECTION_DESCRIPTIONS = {
|
||||
"proxy": "配置不正确将导致 403 错误。服务首次请求 Grok 时的 IP 必须与获取 CF Clearance 时的 IP 一致,后续服务器请求 IP 变化不会导致 403。"
|
||||
"proxy": "配置不正确将导致 403 错误。服务首次请求 Grok 时的 IP 必须与获取 CF Clearance 时的 IP 一致,后续服务器请求 IP 变化不会导致 403。",
|
||||
"model_routing": "这里可以手动指定每个模型优先走哪个 Token 池。未配置的模型仍会按系统默认路由。"
|
||||
};
|
||||
|
||||
// CF 自动刷新联动禁用字段(全部在 proxy section 内)
|
||||
@@ -312,9 +322,28 @@ function randomKey(len) {
|
||||
async function init() {
|
||||
apiKey = await ensureAdminKey();
|
||||
if (apiKey === null) return;
|
||||
await loadModelRoutingMeta();
|
||||
loadData();
|
||||
}
|
||||
|
||||
async function loadModelRoutingMeta() {
|
||||
try {
|
||||
const res = await fetch('/v1/admin/model-routing/meta', {
|
||||
headers: buildAuthHeaders(apiKey)
|
||||
});
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
if (Array.isArray(data.models)) {
|
||||
modelRoutingMeta.models = data.models;
|
||||
}
|
||||
if (Array.isArray(data.pools) && data.pools.length) {
|
||||
modelRoutingMeta.pools = data.pools;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('模型池路由元数据加载失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
try {
|
||||
const res = await fetch('/v1/admin/config', {
|
||||
@@ -386,6 +415,182 @@ function renderConfig(data) {
|
||||
applyCfRefreshState(cfEnabled);
|
||||
}
|
||||
|
||||
function normalizeModelRoutingAssignments(raw) {
|
||||
const next = {};
|
||||
const source = raw && typeof raw === 'object' ? raw : {};
|
||||
Object.entries(source).forEach(([modelId, value]) => {
|
||||
if (typeof value === 'string' && value.trim()) {
|
||||
next[modelId] = value.trim();
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
const first = value.map(item => String(item).trim()).find(Boolean);
|
||||
if (first) next[modelId] = first;
|
||||
}
|
||||
});
|
||||
return next;
|
||||
}
|
||||
|
||||
function buildModelRoutingInput(section, key, val) {
|
||||
modelRoutingAssignments = normalizeModelRoutingAssignments(val);
|
||||
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'routing-board';
|
||||
wrapper.dataset.type = 'model-routing';
|
||||
wrapper.dataset.section = section;
|
||||
wrapper.dataset.key = key;
|
||||
|
||||
const leftPane = document.createElement('div');
|
||||
leftPane.className = 'routing-pane routing-pane-models';
|
||||
leftPane.innerHTML = `
|
||||
<div class="routing-pane-header">
|
||||
<div class="routing-pane-title">模型列表</div>
|
||||
<div class="routing-pane-subtitle">拖到右侧池中即可指定路由,拖回这里表示按系统默认池处理。</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const unassignedZone = document.createElement('div');
|
||||
unassignedZone.className = 'routing-dropzone routing-dropzone-source';
|
||||
unassignedZone.dataset.pool = '';
|
||||
registerRoutingDropzone(unassignedZone, '');
|
||||
|
||||
const rightPane = document.createElement('div');
|
||||
rightPane.className = 'routing-pane routing-pane-pools';
|
||||
rightPane.innerHTML = `
|
||||
<div class="routing-pane-header">
|
||||
<div class="routing-pane-title">Token 池</div>
|
||||
<div class="routing-pane-subtitle">模型放到哪个池,后端请求就优先走哪个池。</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const poolGrid = document.createElement('div');
|
||||
poolGrid.className = 'routing-pool-grid';
|
||||
const pools = Array.isArray(modelRoutingMeta.pools) && modelRoutingMeta.pools.length
|
||||
? modelRoutingMeta.pools
|
||||
: ['ssoBasic', 'ssoSuper'];
|
||||
|
||||
pools.forEach(poolName => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'routing-pool-card';
|
||||
|
||||
const header = document.createElement('div');
|
||||
header.className = 'routing-pool-header';
|
||||
header.innerHTML = `
|
||||
<div class="routing-pool-title">${poolName}</div>
|
||||
<div class="routing-pool-hint">拖拽模型到这里</div>
|
||||
`;
|
||||
|
||||
const zone = document.createElement('div');
|
||||
zone.className = 'routing-dropzone routing-dropzone-pool';
|
||||
zone.dataset.pool = poolName;
|
||||
registerRoutingDropzone(zone, poolName);
|
||||
|
||||
card.appendChild(header);
|
||||
card.appendChild(zone);
|
||||
poolGrid.appendChild(card);
|
||||
});
|
||||
|
||||
leftPane.appendChild(unassignedZone);
|
||||
rightPane.appendChild(poolGrid);
|
||||
wrapper.appendChild(leftPane);
|
||||
wrapper.appendChild(rightPane);
|
||||
|
||||
renderModelRoutingBoard(wrapper);
|
||||
return { input: wrapper, node: wrapper };
|
||||
}
|
||||
|
||||
function registerRoutingDropzone(zone, poolName) {
|
||||
zone.addEventListener('dragover', (event) => {
|
||||
event.preventDefault();
|
||||
zone.classList.add('is-over');
|
||||
});
|
||||
zone.addEventListener('dragleave', () => {
|
||||
zone.classList.remove('is-over');
|
||||
});
|
||||
zone.addEventListener('drop', (event) => {
|
||||
event.preventDefault();
|
||||
zone.classList.remove('is-over');
|
||||
const modelId = event.dataTransfer.getData('text/plain') || modelRoutingDragId;
|
||||
if (!modelId) return;
|
||||
if (poolName) {
|
||||
modelRoutingAssignments[modelId] = poolName;
|
||||
} else {
|
||||
delete modelRoutingAssignments[modelId];
|
||||
}
|
||||
const board = zone.closest('.routing-board');
|
||||
if (board) renderModelRoutingBoard(board);
|
||||
modelRoutingDragId = '';
|
||||
});
|
||||
}
|
||||
|
||||
function createModelChip(model) {
|
||||
const chip = document.createElement('button');
|
||||
chip.type = 'button';
|
||||
chip.className = 'routing-chip';
|
||||
chip.draggable = true;
|
||||
chip.dataset.modelId = model.id;
|
||||
chip.innerHTML = `
|
||||
<span class="routing-chip-name">${model.display_name || model.id}</span>
|
||||
<span class="routing-chip-id">${model.id}</span>
|
||||
`;
|
||||
|
||||
chip.addEventListener('dragstart', (event) => {
|
||||
modelRoutingDragId = model.id;
|
||||
event.dataTransfer.setData('text/plain', model.id);
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
chip.classList.add('is-dragging');
|
||||
});
|
||||
chip.addEventListener('dragend', () => {
|
||||
chip.classList.remove('is-dragging');
|
||||
modelRoutingDragId = '';
|
||||
document.querySelectorAll('.routing-dropzone.is-over').forEach((node) => {
|
||||
node.classList.remove('is-over');
|
||||
});
|
||||
});
|
||||
|
||||
return chip;
|
||||
}
|
||||
|
||||
function renderModelRoutingBoard(board) {
|
||||
const models = Array.isArray(modelRoutingMeta.models) ? [...modelRoutingMeta.models] : [];
|
||||
models.sort((a, b) => String(a.id).localeCompare(String(b.id)));
|
||||
|
||||
const sourceZone = board.querySelector('.routing-dropzone-source');
|
||||
const poolZones = Array.from(board.querySelectorAll('.routing-dropzone-pool'));
|
||||
if (!sourceZone) return;
|
||||
|
||||
sourceZone.replaceChildren();
|
||||
poolZones.forEach(zone => zone.replaceChildren());
|
||||
|
||||
const assignedModelIds = new Set();
|
||||
poolZones.forEach(zone => {
|
||||
const poolName = zone.dataset.pool || '';
|
||||
const assigned = models.filter(model => modelRoutingAssignments[model.id] === poolName);
|
||||
assigned.forEach(model => {
|
||||
assignedModelIds.add(model.id);
|
||||
zone.appendChild(createModelChip(model));
|
||||
});
|
||||
if (!assigned.length) {
|
||||
zone.appendChild(createRoutingEmptyState('拖拽模型到这里'));
|
||||
}
|
||||
});
|
||||
|
||||
const unassigned = models.filter(model => !assignedModelIds.has(model.id));
|
||||
unassigned.forEach(model => {
|
||||
sourceZone.appendChild(createModelChip(model));
|
||||
});
|
||||
if (!unassigned.length) {
|
||||
sourceZone.appendChild(createRoutingEmptyState('所有模型都已分配到池'));
|
||||
}
|
||||
}
|
||||
|
||||
function createRoutingEmptyState(text) {
|
||||
const empty = document.createElement('div');
|
||||
empty.className = 'routing-empty';
|
||||
empty.textContent = text;
|
||||
return empty;
|
||||
}
|
||||
|
||||
function applyCfRefreshState(enabled) {
|
||||
// 设置字段禁用状态的辅助函数
|
||||
function setFieldDisabled(section, key, disabled) {
|
||||
@@ -468,7 +673,11 @@ function buildFieldCard(section, key, val) {
|
||||
]);
|
||||
}
|
||||
else if (Array.isArray(val) || typeof val === 'object') {
|
||||
built = buildJsonInput(section, key, val);
|
||||
if (section === 'model_routing' && key === 'model_pools') {
|
||||
built = buildModelRoutingInput(section, key, val);
|
||||
} else {
|
||||
built = buildJsonInput(section, key, val);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (key === 'api_key' || key === 'app_key' || key === 'public_key') {
|
||||
@@ -522,11 +731,16 @@ async function saveConfig() {
|
||||
const newConfig = typeof structuredClone === 'function'
|
||||
? structuredClone(currentConfig)
|
||||
: JSON.parse(JSON.stringify(currentConfig));
|
||||
const inputs = document.querySelectorAll('input[data-section], textarea[data-section], select[data-section]');
|
||||
const inputs = document.querySelectorAll('input[data-section], textarea[data-section], select[data-section], [data-type="model-routing"][data-section]');
|
||||
|
||||
inputs.forEach(input => {
|
||||
const s = input.dataset.section;
|
||||
const k = input.dataset.key;
|
||||
if (input.dataset.type === 'model-routing') {
|
||||
if (!newConfig[s]) newConfig[s] = {};
|
||||
newConfig[s][k] = { ...modelRoutingAssignments };
|
||||
return;
|
||||
}
|
||||
let val = input.value;
|
||||
|
||||
if (input.type === 'checkbox') {
|
||||
|
||||
@@ -1405,8 +1405,8 @@
|
||||
|
||||
async function loadModels() {
|
||||
if (!modelSelect) return;
|
||||
const fallback = ['grok-4.1-fast', 'grok-4', 'grok-3', 'grok-3-mini', 'grok-3-thinking', 'grok-4.20-beta'];
|
||||
const preferred = 'grok-4.20-beta';
|
||||
const fallback = ['grok-4.1-fast', 'grok-4', 'grok-3', 'grok-3-mini', 'grok-3-thinking', 'grok-4.20-fast', 'grok-4.20-expert', 'grok-4.20-auto'];
|
||||
const preferred = 'grok-4.20-auto';
|
||||
let list = fallback;
|
||||
|
||||
try {
|
||||
|
||||
@@ -81,6 +81,13 @@ save_delay_ms = 500
|
||||
# 多 worker 状态同步间隔(秒)
|
||||
reload_interval_sec = 30
|
||||
|
||||
# ==================== 模型路由配置 ====================
|
||||
[model_routing]
|
||||
# 模型到 Token 池的手动路由配置。
|
||||
# 值支持字符串(单池)或字符串数组(按顺序回退)。
|
||||
# 例如:{ "grok-4.20-expert" = "ssoSuper", "grok-4.20-auto" = ["ssoBasic", "ssoSuper"] }
|
||||
model_pools = {}
|
||||
|
||||
# ==================== 缓存管理 ====================
|
||||
[cache]
|
||||
# 是否启用自动清理
|
||||
|
||||
Reference in new issue
Block a user