mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 13:38:31 +00:00
# v0.5.20 (2026-07-07)
## Features - **Thinking**: per-model thinking level picker on provider page — appends `(level)` suffix to copied model names for forced reasoning effort across all formats (openai, claude, gemini, deepseek, kimi, qwen, zai, minimax, hunyuan, step) - **RTK**: add JS-native git-log filter (#2423) - **Caveman**: add targeted upstream-aligned style rules (#2424) - **i18n**: add Farsi (fa) language support (#2385) ## Fixes - **Thinking**: strip `(level)` suffix from upstream `body.model` so providers no longer reject requests - **Translator**: preserve developer instructions in openai-responses conversion (#2434) - **count_tokens**: count structured Anthropic blocks (#2419) - **Volcengine-ark**: clamp GLM-5 max_tokens to model output ceiling (#2428) - **Kimi**: normalize reasoning_effort to backend enum (#2427) - **Claude**: reconcile max_tokens vs thinking budget and lift per-model ceiling (#2381) - **Kiro**: deliver system prompt natively, add Opus 4.5/4.7/4.8, tolerate dash version ids (#2366) - **Headroom**: proxy dashboard through app (#2372) - **MITM**: recover from stale lock file on server start
This commit is contained in:
@@ -63,14 +63,19 @@ export function getModelType(aliasOrId, modelId) {
|
||||
}
|
||||
|
||||
export function getModelUpstreamId(aliasOrId, modelId) {
|
||||
// Split off thinking suffix "(level)" so lookup hits the base id; re-append it to
|
||||
// the result so downstream applyThinking still sees the suffix (body.model is stripped separately).
|
||||
const sufMatch = typeof modelId === "string" ? modelId.match(/\([^()]+\)\s*$/) : null;
|
||||
const suffix = sufMatch ? sufMatch[0] : "";
|
||||
const baseId = suffix ? modelId.slice(0, sufMatch.index).trim() : modelId;
|
||||
const models = PROVIDER_MODELS[aliasOrId];
|
||||
const found = findModel(models, modelId, aliasOrId);
|
||||
if (found?.upstreamModelId) return found.upstreamModelId;
|
||||
if (found?.id) return found.id;
|
||||
if (aliasOrId === "cx" && typeof modelId === "string" && modelId.endsWith(CODEX_REVIEW_SUFFIX)) {
|
||||
return modelId.slice(0, -CODEX_REVIEW_SUFFIX.length);
|
||||
const found = findModel(models, baseId, aliasOrId);
|
||||
if (found?.upstreamModelId) return found.upstreamModelId + suffix;
|
||||
if (found?.id) return found.id + suffix;
|
||||
if (aliasOrId === "cx" && typeof baseId === "string" && baseId.endsWith(CODEX_REVIEW_SUFFIX)) {
|
||||
return baseId.slice(0, -CODEX_REVIEW_SUFFIX.length) + suffix;
|
||||
}
|
||||
return modelId;
|
||||
return baseId + suffix;
|
||||
}
|
||||
|
||||
export function getModelQuotaFamily(aliasOrId, modelId) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { detectFormat, getTargetFormat, resolveTransport } from "../services/provider.js";
|
||||
import { translateRequest } from "../translator/index.js";
|
||||
import { stripThinkingSuffix } from "../translator/concerns/thinkingUnified.js";
|
||||
import { FORMATS } from "../translator/formats.js";
|
||||
import { normalizeClaudePassthrough } from "../translator/formats/claude.js";
|
||||
import { COLORS } from "../utils/stream.js";
|
||||
@@ -123,9 +124,9 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred
|
||||
let toolNameMap;
|
||||
if (passthrough) {
|
||||
log?.debug?.("PASSTHROUGH", `${clientTool} → ${provider} | native lossless`);
|
||||
translatedBody = { ...body, model: upstreamModel };
|
||||
translatedBody = { ...body, model: stripThinkingSuffix(upstreamModel) };
|
||||
// Normalize newer Cowork/CC beta shapes (adaptive thinking, mid-conversation system) the API rejects
|
||||
if (clientTool === "claude") normalizeClaudePassthrough(translatedBody, upstreamModel);
|
||||
if (clientTool === "claude") normalizeClaudePassthrough(translatedBody, translatedBody.model);
|
||||
} else {
|
||||
translatedBody = translateRequest(sourceFormat, targetFormat, upstreamModel, body, stream, credentials, provider, reqLogger, stripList, connectionId, clientTool);
|
||||
if (!translatedBody) {
|
||||
@@ -134,7 +135,7 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred
|
||||
}
|
||||
toolNameMap = translatedBody._toolNameMap;
|
||||
delete translatedBody._toolNameMap;
|
||||
translatedBody.model = upstreamModel;
|
||||
translatedBody.model = stripThinkingSuffix(upstreamModel);
|
||||
}
|
||||
|
||||
// Dedupe duplicate built-in tools when equivalent MCP tools are present (Claude clients only).
|
||||
|
||||
@@ -60,12 +60,10 @@ export default {
|
||||
},
|
||||
},
|
||||
models: [
|
||||
{ id: "claude-fable-5", name: "Claude Fable 5" },
|
||||
{ id: "claude-sonnet-5", name: "Claude Sonnet 5" },
|
||||
{ id: "claude-opus-4-8", name: "Claude Opus 4.8" },
|
||||
{ id: "claude-opus-4-7", name: "Claude Opus 4.7" },
|
||||
{ id: "claude-opus-4-6", name: "Claude Opus 4.6" },
|
||||
{ id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6" },
|
||||
{ id: "claude-opus-4-5-20251101", name: "Claude 4.5 Opus" },
|
||||
{ id: "claude-sonnet-4-5-20250929", name: "Claude 4.5 Sonnet" },
|
||||
{ id: "claude-haiku-4-5-20251001", name: "Claude 4.5 Haiku" },
|
||||
],
|
||||
oauth: {
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
// Resolve valid thinking levels per model — drives UI level picker (suffix "model(level)").
|
||||
// Reuses capabilities.js (thinkingFormat/canDisable) so this file only maps format→levels (DRY).
|
||||
import { getCapabilitiesForModel } from "./capabilities.js";
|
||||
import { matchPattern } from "./pricing.js";
|
||||
|
||||
// Shared level sets (deduped) — verified against provider docs + wire in thinkingUnified.applyFormat.
|
||||
const L = {
|
||||
base: ["none", "low", "medium", "high"], // qwen, step, hunyuan, gemini-budget
|
||||
onOff: ["none", "thinking"], // zai (binary), minimax (adaptive)
|
||||
openai: ["none", "minimal", "low", "medium", "high", "xhigh"], // GPT-5.x / o-series (no "max")
|
||||
levelMax: ["none", "low", "medium", "high", "max"], // claude-adaptive, kimi
|
||||
budgetX: ["none", "low", "medium", "high", "xhigh", "max"], // claude-budget
|
||||
gemini: ["minimal", "low", "medium", "high"], // gemini-3 thinkingLevel (no disable)
|
||||
hiMax: ["none", "high", "max"], // deepseek (low/med→high, xhigh→max)
|
||||
};
|
||||
|
||||
// thinkingFormat → valid selectable levels (source of truth for UI options).
|
||||
const FORMAT_LEVELS = {
|
||||
openai: L.openai,
|
||||
"claude-adaptive": L.levelMax,
|
||||
"claude-budget": L.budgetX,
|
||||
"gemini-level": L.gemini,
|
||||
"gemini-budget": L.base,
|
||||
zai: L.onOff,
|
||||
qwen: L.base,
|
||||
kimi: L.levelMax,
|
||||
deepseek: L.hiMax,
|
||||
minimax: L.onOff,
|
||||
hunyuan: L.base,
|
||||
step: L.base,
|
||||
};
|
||||
|
||||
// Model-name pattern overrides (glob, first match wins) — more precise than format default.
|
||||
const PATTERN_THINKING = [
|
||||
{ pattern: "*codex*", levels: ["low", "medium", "high", "xhigh"] }, // codex cannot disable thinking
|
||||
];
|
||||
|
||||
// Returns valid thinking levels for a model, or null when the model has no reasoning.
|
||||
export function getThinkingLevels(provider, model) {
|
||||
const caps = getCapabilitiesForModel(provider, model);
|
||||
if (!caps.reasoning) return null;
|
||||
const hit = PATTERN_THINKING.find((p) => matchPattern(p.pattern, model));
|
||||
let levels = hit?.levels || FORMAT_LEVELS[caps.thinkingFormat] || L.base;
|
||||
if (caps.thinkingCanDisable === false) levels = levels.filter((l) => l !== "none");
|
||||
return levels;
|
||||
}
|
||||
@@ -20,6 +20,13 @@ const FORMAT_TO_NATIVE = {
|
||||
kiro: "kiro",
|
||||
};
|
||||
|
||||
// Strip a trailing thinking suffix "model(value)" → "model" (no-op when absent).
|
||||
export function stripThinkingSuffix(model) {
|
||||
if (typeof model !== "string") return model;
|
||||
const m = model.match(/^(.*)\([^()]+\)\s*$/);
|
||||
return m ? m[1].trim() : model;
|
||||
}
|
||||
|
||||
// Parse model-name suffix "model(value)" → { cleanModel, override }.
|
||||
// value: level name (high) | number (8192) | auto | none. null override when absent.
|
||||
export function parseSuffix(model) {
|
||||
|
||||
Reference in New Issue
Block a user