Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
fec93dc
build(deps): 提交 package-lock.json 供 Docker npm ci 使用——上游 9router 时代忽略…
techysy Oct 10, 2026
6c5b3d0
feat(quota): 通用外部额度推送接口——display-only 卡片替代按供应商定制草案
techysy Oct 12, 2026
d766ca1
feat(providers): tokenharbor-free 免费额度卡原生接线——Cookie 直填+探针+双轨会话
techysy Oct 12, 2026
26b39a3
feat(keys): API Key 模型/Combo 白名单——细粒度调用范围管控
techysy Oct 12, 2026
59a9c0e
fix(translator): 上游吸收批次——gemini 重复 tool_call_id 去重+GLM5.2/5.3 能力+curs…
techysy Oct 12, 2026
a55f9a6
feat(media+ui): ElevenLabs Scribe STT+Ollama 缓存 token 统计+移动端布局+Combo …
techysy Oct 12, 2026
dcdefd4
fix(scripts): fpk-local 自动盖号在无历史测试产物时静默死
techysy Oct 12, 2026
669c221
feat(providers): 接入 MiniMax Code 订阅双线,撤除 tokenharbor-free 脏数据并补齐图标
techysy Oct 13, 2026
f85ae33
feat(quota): 配额卡片显示偏好聚合弹窗与外部伴侣卡片保护
techysy Oct 13, 2026
4fb6f50
fix(experimental): 移除多余的 hook 闭合括号
techysy Oct 13, 2026
b9305c8
refactor(experimental): 移除冗余的配额卡片开关,收敛至配额追踪器工具栏弹窗
techysy Oct 13, 2026
b556e39
fix(oauth): OAuthModal 补齐 MiniMax Code 设备码名单,minimax-free 别名定为 mmcf
techysy Oct 13, 2026
7d2bb1d
test(golden): 补录 minimax-code 与 minimax-code-global 的 URL/headers 快照
techysy Oct 14, 2026
ae2e54b
test(quota): 隐藏零余额开关断言改为对齐偏好弹窗
techysy Oct 14, 2026
84b13a9
test(golden): 修正 cline/clinepass/kimi headers 快照并重生成基线
techysy Oct 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ source/*
.cursor/*
docs/*
!docs/ARCHITECTURE.md
!docs/en/*.md
!docs/zh-CN/*.md
test/*
open-sse/test/*
RM.vn.md
Expand All @@ -62,7 +64,9 @@ PUBLIC.md
Thanks.md
PUBLIC.en.md
PR/*
package-lock.json
# package-lock.json is TRACKED (upstream 9router used to ignore it): the Docker
# build runs `npm ci` against it for reproducible installs — build-time registry
# drift hung the QEMU arm64 leg for 6h during v1.4.0. Keep it committed.


#Ignore vscode AI rules
Expand Down
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,29 @@

> 面向用户的精简更新见 [`public/i18n/changelog/`](https://github.com/techysy/10router/tree/main/public/i18n/changelog)(`en.md` / `zh-CN.md` / `zh-TW.md`,仪表盘「Change Log」按界面语言加载对应文件)。本文件为完整开发日志,按版本从上往下排列。

## v1.4.1 (2026-10-12)

### ✨ 新增

- **API Key 细粒度权限管控(模型与 Combo 白名单)**:为客户端 API Key 新增可调用的模型白名单(`allowedModels`)与 Combo 白名单(`allowedCombos`)。未配置时默认无限制;配置后请求不在白名单内的模型或组合立即返回 403 Forbidden(`model_not_allowed`);同时在带 Key 访问 `GET /v1/models` 时自动按白名单过滤返回的模型列表。密钥轮换继承白名单配置;数据库表 `apiKeys` 自动无损升级增加字段(SCHEMA_VERSION 2→3);前端 Endpoint 页提供交互式白名单编辑弹窗并在密钥列表中直观显示限制状态。
- **通用外部额度推送接口(仪表盘配额页的 display-only 卡片)**:外部伴侣应用(首个接线方为 CreditDaddy)在**自己那一侧**解析好某供应商的额度后,把它 POST 到 `POST /api/external-quota/<source>`,10Router 就在统一配额页把这些数字渲染成卡片——与 OAuth/API key 连接同一面墙、同一套分段条 / 逐包明细 / 隐藏行 / 零余额过滤,不另建外挂页面。鉴权与 `GET /api/usage/quotas` 同信任级:虚拟 key(`Bearer sk-…`)或本机 CLI token,路由自身再验一遍(`requireLogin=false` 的内网免登录部署除外——与仪表盘其他写路由同姿态)。**这条路径只接收额度数字**:载荷字段白名单 + 标量强制(嵌套对象/数组整体 400 拒绝,而不是静默剥离——cookie/token 材料从构造上就进不来),配额值以外的未知标量字段丢弃。快照存 `_meta` 的 `externalQuota.<source>`,7 天 TTL:推送方停更一周后卡片自动消失,不会长挂死数。卡片删除按钮走 `DELETE /api/external-quota/<source>`(推送方下次更新即恢复);推送卡在 `GET /api/providers/client` 里与真实连接合并返回(合成行 `ext:<source>`,不进 providers 表、不参与路由/账号轮换),置顶排序、数字随行内联下发,客户端不发 `/api/usage/<id>`(无上游可拉,避免永久 spinner)。载荷契约与推送节奏写进 CreditDaddy 侧 issue。
顺带修正两处通用渲染链对标志位的吞失:`parseQuotaData` 的 default 分支此前只搬 `used/total/resetAt`,把 `unlimited/recurring/percentScale` 等展示标志丢掉(0/0 的 unlimited 窗口会渲染成耗尽),现在原样透传。
- **Gemini / Antigravity 并发与重复 `tool_call_id` 自动去重**:在 OpenAI 转换为 Gemini/Antigravity 请求时,若客户端或合并流产生重复的 `tool_call_id`,转换层自动追加唯一标识并在对应的 `functionResponse` 中保持严格映射,避免上游 Google 拒绝 400(Duplicate function call ID)。
- **GLM-5.2 / 5.3 能力声明校准**:GLM-5.2 与 GLM-5.3 上下文窗口提升至真实的 1000000(1M);GLM-5.3 默认不可关闭 thinking(`thinkingCanDisable: false`)。
- **Cursor 协议转发增强**:AgentService 模式下补充 `reasoning_effort` 字段透传;在消息处理中过滤掉无文本无工具的纯空轮次,防止 Cursor 上游抛出 400。
- **ElevenLabs Scribe 语音转文字(STT)**:媒体提供商 `elevenlabs` 接入 Scribe 模型(`scribe_v1`),支持标准 multipart 上传音频与多语言转录,并对齐 `xi-api-key` 鉴权头。
- **Ollama 提示词缓存 Token 纳入统计**:解析 Ollama 响应中的 `prompt_tokens_details.cached_tokens` 并映射到 OpenAI 兼容的 `cached_tokens` 字段,使本地 Ollama 请求的上下文缓存命中量能在仪表盘与用量明细中正确追踪。
- **Kimi Code `/responses` 直连端点**:在 `kimi` 供应商注册表中新增 responses 格式直连配置,无缝对接 Kimi 官方编程助手的新一代长思考端点(`https://api.kimi.com/coding/v1/responses`)。
- **移动端自适应布局与防溢出**:修复手机端访问 Endpoint 密钥列表、Provider Models 以及 CLI Tools 卡片时出现的横向滚动条与按钮挤压问题;超长模型名在小屏下优雅自适应截断并支持悬停复制。
- **Combo 挑选器展示未连接模型**:在组合(Combo)编辑面板的模型选择器中,即使尚未配置某供应商的连接,该供应商的兼容模型也会以“未连接”标签清晰呈现并支持提前选入组合编排。
- **MiniMax Code 订阅双线(OAuth 设备码直连与用量追踪)**:新增 `minimax-code`(国内站)与 `minimax-code-global`(国际站)两条编程助手订阅直连线。支持通过设备码流程(PKCE S256,client `mcode-public`)登录,直连 MiniMax Agent Mavis 网关;具备单飞防并发的单次刷新令牌轮换机制;内置用量探针自动抓取 M Plan 速率限制窗口并 fail-soft 读取 Credits 余额。别名 `mcode` 与 `mm` 正式指向国内订阅线(国际线挂 `mmg`/`mmcode`),免费线 `minimax-free` 对应短别名优化为 `mmcf`。
- **OAuthModal 设备码列表补齐**:修复 OAuth 弹窗中 `deviceCodeProviders` 遗漏 `minimax-code` 与 `minimax-code-global` 导致点击连接时误入授权码模式报错的问题。
- **配额卡片显示偏好聚合弹窗与外部卡片保护**:在配额页工具栏(QuotaToolbar)新增「配额卡片显示偏好」弹窗(图标 tune),将原本散落在实验性页面的「隐藏无配额卡片」「隐藏零余额卡片」「嵌套同心圆环进度条」与新增的「显示外部推送卡片」开关聚合在同一弹窗中统一管理。同时优化过滤判定规则:外部推送卡片(`conn.external`,来自 CreditDaddy 等)拥有独立的显隐开关,彻底不受无配额/零余额的误伤;且带有错误提示(如网页会话失效需重新登录)的卡片始终强制保留呈现,确保用户不会漏看关键会话告警。

### 🔧 清理

- **移除误提交的 Token Harbor 外挂额度草案**(fec93dcc 随 lockfile 一起混入的三个文件 + Sidebar 入口,从未发版):评估后确认不需要按供应商定制的免费额度卡片——上面的通用推送接口以 display-only 方式覆盖同类需求,凭据解析留在推送方。`tokenharbor` 注册表条目保留独立的修正不受影响:API key 卡片的 `priority: 45` 置顶(新装默认视图内可见)。

## v1.4.0 (2026-10-10)

### ✨ 新增
Expand Down
19 changes: 14 additions & 5 deletions open-sse/executors/cursor.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,13 +95,18 @@ function encodeHistoryMessage(message) {
return agentMessage(1, agentMessage(1, agentMessage(1, text)));
}

function buildAgentRunFrame(messages, model) {
const system = messages
function buildAgentRunFrame(messages, model, reasoningEffort = null) {
const cleanMessages = (messages || []).filter((msg) => {
if (!msg) return false;
const text = textFromContent(msg.content);
return Boolean(text || msg.tool_calls?.length || msg.role === "system");
});
const system = cleanMessages
.filter((message) => message?.role === "system")
.map((message) => textFromContent(message.content))
.filter(Boolean)
.join("\n\n");
const chatMessages = messages.filter((message) => message?.role !== "system");
const chatMessages = cleanMessages.filter((message) => message?.role !== "system");
const currentIndex = [...chatMessages].map((message) => message?.role).lastIndexOf("user");
const current = currentIndex >= 0 ? chatMessages[currentIndex] : chatMessages.at(-1);
const history = chatMessages
Expand All @@ -123,7 +128,11 @@ function buildAgentRunFrame(messages, model) {
...(conversationHistory ? [agentMessage(7, conversationHistory)] : []),
);
const conversationAction = agentMessage(1, userAction);
const requestedModel = concatBuffers(agentString(1, model), agentBool(7, true));
const requestedModel = concatBuffers(
agentString(1, model),
agentBool(7, true),
...(reasoningEffort ? [agentString(8, String(reasoningEffort))] : [])
);
const runRequest = concatBuffers(
// An empty ConversationStateStructure starts a fresh local agent session.
agentMessage(1, new Uint8Array()),
Expand Down Expand Up @@ -493,7 +502,7 @@ export class CursorExecutor extends BaseExecutor {
let session;
try {
session = this.openAgentHttp2Stream(url, headers, requestController.signal);
session.write(buildAgentRunFrame(body.messages || [], model));
session.write(buildAgentRunFrame(body.messages || [], model, body.reasoning_effort));
} catch (error) {
throw new Error(`Cursor AgentService request failed: ${error.message}`);
}
Expand Down
11 changes: 10 additions & 1 deletion open-sse/executors/default.js
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,9 @@ export class DefaultExecutor extends BaseExecutor {
clinepass: () => this.refreshCline(credentials.refreshToken, proxyOptions),
kimi: () => this.refreshKimi(credentials, proxyOptions),
"kimi-coding": () => this.refreshKimi(credentials, proxyOptions),
kilocode: () => this.refreshKilocode(credentials.refreshToken, proxyOptions)
kilocode: () => this.refreshKilocode(credentials.refreshToken, proxyOptions),
"minimax-code": () => this.refreshMinimaxCode("minimax-code", credentials, proxyOptions),
"minimax-code-global": () => this.refreshMinimaxCode("minimax-code-global", credentials, proxyOptions),
};

const refresher = refreshers[this.provider];
Expand Down Expand Up @@ -354,6 +356,13 @@ export class DefaultExecutor extends BaseExecutor {
return { accessToken: tokens.access_token, refreshToken: tokens.refresh_token || refreshToken, expiresIn: tokens.expires_in };
}

async refreshMinimaxCode(provider, credentials, proxyOptions = null) {
const refreshToken = credentials?.refreshToken;
if (!refreshToken) return null;
const { refreshMinimaxCodeToken } = await import("../services/tokenRefresh/providers.js");
return refreshMinimaxCodeToken(provider, refreshToken, console, proxyOptions);
}

async refreshKilocode(refreshToken, proxyOptions = null) {
// Kilocode uses device code flow, no refresh token support
return null;
Expand Down
17 changes: 17 additions & 0 deletions open-sse/handlers/sttCore.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ function buildAuthHeaders(cfg, token) {
case "bearer": return { "Authorization": `Bearer ${token}` };
case "token": return { "Authorization": `Token ${token}` };
case "x-api-key": return { "x-api-key": token };
case "xi-api-key": return { "xi-api-key": token };
case "key": return { "Authorization": `Key ${token}` };
default: return { "Authorization": `Bearer ${token}` };
}
Expand Down Expand Up @@ -138,6 +139,21 @@ async function transcribeHuggingFace(cfg, file, model, token) {
return jsonResponse({ text: data.text || "" });
}

// ElevenLabs Scribe STT: multipart with model_id
async function transcribeElevenLabs(cfg, file, model, token, formData) {
const fd = new FormData();
fd.append("file", file, file.name || "audio.wav");
fd.append("model_id", model || "scribe_v1");
const lang = formData.get("language");
if (lang && typeof lang === "string" && lang.trim()) {
fd.append("language_code", lang.trim());
}
const res = await fetch(cfg.baseUrl, { method: "POST", headers: buildAuthHeaders(cfg, token), body: fd });
if (!res.ok) return upstreamError(res);
const data = await res.json();
return jsonResponse({ text: data.text || "" });
}

// Default: OpenAI/Groq/Whisper-compatible multipart
async function transcribeOpenAICompatible(cfg, file, model, token, formData) {
const fd = new FormData();
Expand Down Expand Up @@ -231,6 +247,7 @@ export async function handleSttCore({ provider, model, formData, credentials, st
case "nvidia-asr": return await transcribeNvidia(cfg, file, model, token);
case "huggingface-asr": return await transcribeHuggingFace(cfg, file, model, token);
case "gemini-stt": return await transcribeGemini(cfg, file, model, token, formData);
case "elevenlabs": return await transcribeElevenLabs(cfg, file, model, token, formData);
default: return await transcribeOpenAICompatible(cfg, file, model, token, formData);
}
} catch (err) {
Expand Down
4 changes: 3 additions & 1 deletion open-sse/providers/capabilities.js
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ export const MODEL_CAPABILITIES = {
// `*qwen*` / `*mimo*` / `hy3*` / `*grok-4.5*` / `*gpt-5*` 都是跨供应商共享的兜底,
// 改它们会波及别人,因此只加精确行。来源都是第一方 models.dev 条目;与转售商冲突时
// 取第一方。
"glm-5.3": { reasoning: true, thinkingFormat: "zai", contextWindow: 1000000, maxOutput: 131072 }, // zai + zhipuai 一致;`*glm-5*` 给的 200000/128000 是 GLM-4.x 时代的旧值
"glm-5.3": { reasoning: true, thinkingFormat: "zai", thinkingCanDisable: false, contextWindow: 1000000, maxOutput: 131072 }, // zai + zhipuai 一致;thinking 不可关闭
"glm-5.2": { reasoning: true, thinkingFormat: "zai", contextWindow: 1000000, maxOutput: 131072 }, // zhipuai:窗口 1M
"glm-5.1": { reasoning: true, thinkingFormat: "zai", contextWindow: 200000, maxOutput: 131072 }, // zhipuai:窗口与兜底同值,只有输出上限要改
"glm-5": { reasoning: true, thinkingFormat: "zai", contextWindow: 204800, maxOutput: 131072 }, // zai + zhipuai(models.dev 已标 deprecated)
Expand Down Expand Up @@ -809,6 +809,8 @@ export const PATTERN_CAPABILITIES = [
{ pattern: "*kimi*", caps: { reasoning: true, thinkingFormat: "kimi", contextWindow: 262144 } },

// ── GLM / Z.ai (thinking.enabled; disable via enable_thinking:false) ─
{ pattern: "*glm-5.3*", caps: { reasoning: true, thinkingFormat: "zai", thinkingCanDisable: false, contextWindow: 1000000, maxOutput: 128000 } },
{ pattern: "*glm-5.2*", caps: { reasoning: true, thinkingFormat: "zai", contextWindow: 1000000, maxOutput: 128000 } },
{ pattern: "*glm-5*", caps: { reasoning: true, thinkingFormat: "zai", contextWindow: 200000, maxOutput: 128000 } },
{ pattern: "*glm-4.7*", caps: { reasoning: true, thinkingFormat: "zai", contextWindow: 200000, maxOutput: 128000 } },
{ pattern: "*glm-4*", caps: { reasoning: true, thinkingFormat: "zai", contextWindow: 200000 } },
Expand Down
15 changes: 14 additions & 1 deletion open-sse/providers/registry/elevenlabs.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ export default {
category: "apikey",
authType: "apikey",
serviceKinds: [
"tts"
"tts",
"stt"
],
ttsConfig: {
baseUrl: "https://api.elevenlabs.io/v1/text-to-speech",
Expand All @@ -31,5 +32,17 @@ export default {
name: "Eleven Turbo v2.5"
}
]
},
sttConfig: {
baseUrl: "https://api.elevenlabs.io/v1/speech-to-text",
authType: "apikey",
authHeader: "xi-api-key",
format: "elevenlabs",
models: [
{
id: "scribe_v1",
name: "Scribe v1"
}
]
}
};
Loading
Loading