# 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:
decolua
2026-07-07 16:29:11 +07:00
parent 081c6f2aff
commit b10b807063
11 changed files with 137 additions and 41 deletions
@@ -1,7 +1,8 @@
import PropTypes from "prop-types";
import { CapacityBadges } from "@/shared/components";
export default function ModelRow({ model, fullModel, alias, copied, onCopy, testStatus, isCustom, isFree, onDeleteAlias, onTest, isTesting, onDisable, caps }) {
export default function ModelRow({ model, fullModel, alias, copied, onCopy, testStatus, isCustom, isFree, onDeleteAlias, onTest, isTesting, onDisable, caps, thinkingSuffix }) {
const displayModel = thinkingSuffix ? `${fullModel}(${thinkingSuffix})` : fullModel;
const borderColor = testStatus === "ok"
? "border-green-500/40"
: testStatus === "error"
@@ -24,7 +25,7 @@ export default function ModelRow({ model, fullModel, alias, copied, onCopy, test
{testStatus === "ok" ? "check_circle" : testStatus === "error" ? "cancel" : "smart_toy"}
</span>
<div className="flex min-w-0 flex-1 flex-col gap-1">
<code className="max-w-[72vw] truncate rounded bg-sidebar px-1.5 py-0.5 font-mono text-xs text-text-muted sm:max-w-[360px]">{fullModel}</code>
<code className="max-w-[72vw] truncate rounded bg-sidebar px-1.5 py-0.5 font-mono text-xs text-text-muted sm:max-w-[360px]">{displayModel}</code>
<span className="flex min-w-0 items-center text-[9px] gap-1 pl-1">
{model.name && <span className="truncate text-[9px] italic text-text-muted/70">{model.name}</span>}
<CapacityBadges caps={caps} colorOverride="text-text-muted/70" size={12} />
@@ -48,7 +49,7 @@ export default function ModelRow({ model, fullModel, alias, copied, onCopy, test
)}
<div className="relative shrink-0 group/btn">
<button
onClick={() => onCopy(fullModel, `model-${model.id}`)}
onClick={() => onCopy(displayModel, `model-${model.id}`)}
className="rounded p-0.5 text-text-muted hover:bg-sidebar hover:text-primary"
>
<span className="material-symbols-outlined text-sm">
@@ -97,4 +98,5 @@ ModelRow.propTypes = {
isTesting: PropTypes.bool,
onDisable: PropTypes.func,
caps: PropTypes.object,
thinkingSuffix: PropTypes.string,
};
@@ -5,8 +5,9 @@ import { useParams, useRouter } from "next/navigation";
import Link from "next/link";
import Image from "next/image";
import { Card, Button, Badge, Input, Modal, CardSkeleton, OAuthModal, KiroOAuthWrapper, CursorAuthModal, IFlowCookieModal, GitLabAuthModal, Toggle, Select, EditConnectionModal, NoAuthProxyCard, ConfirmModal } from "@/shared/components";
import { OAUTH_PROVIDERS, APIKEY_PROVIDERS, FREE_PROVIDERS, FREE_TIER_PROVIDERS, WEB_COOKIE_PROVIDERS, getProviderAlias, isOpenAICompatibleProvider, isAnthropicCompatibleProvider, AI_PROVIDERS, THINKING_CONFIG } from "@/shared/constants/providers";
import { OAUTH_PROVIDERS, APIKEY_PROVIDERS, FREE_PROVIDERS, FREE_TIER_PROVIDERS, WEB_COOKIE_PROVIDERS, getProviderAlias, isOpenAICompatibleProvider, isAnthropicCompatibleProvider, AI_PROVIDERS } from "@/shared/constants/providers";
import { getModelsByProviderId, getModelKind } from "@/shared/constants/models";
import { getThinkingLevels } from "open-sse/providers/thinkingLevels.js";
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
import { useModelCaps } from "@/shared/hooks/useModelCaps";
import { translate } from "@/i18n/runtime";
@@ -149,7 +150,21 @@ export default function ProviderDetailPage() {
const hasDualAuthModes = !isCompatible && isOAuth && supportsApiKeyAuth;
const oauthConnectionLabel = providerId === "xai" ? "Grok Build OAuth" : "OAuth";
const apiKeyConnectionLabel = providerId === "xai" ? "xAI API Key" : "API Key";
const thinkingConfig = AI_PROVIDERS[providerId]?.thinkingConfig || THINKING_CONFIG.extended;
// Resolve suffix "(level)" for a model when a thinking level is picked and the model supports it.
const resolveThinkingSuffix = (modelId) => {
if (!thinkingMode || thinkingMode === "auto") return null;
const levels = getThinkingLevels(providerId, modelId);
return levels && levels.includes(thinkingMode) ? thinkingMode : null;
};
// Union of levels across this provider's reasoning models — drives the level picker options.
const providerThinkingLevels = (() => {
const set = new Set();
for (const m of models) {
const lv = getThinkingLevels(providerId, m.id);
if (lv) lv.forEach((l) => { if (l !== "none") set.add(l); });
}
return set.size ? ["auto", ...[...set]] : null;
})();
const providerStorageAlias = isCompatible ? providerId : providerAlias;
const providerDisplayAlias = isCompatible
@@ -1065,6 +1080,7 @@ export default function ProviderDetailPage() {
isCustom
isFree={false}
caps={getCaps(`${providerId}/${model.id}`)}
thinkingSuffix={resolveThinkingSuffix(model.id)}
/>
))}
@@ -1090,6 +1106,7 @@ export default function ProviderDetailPage() {
isFree={model.isFree}
onDisable={() => handleDisableModel(model.id)}
caps={getCaps(`${providerId}/${model.id}`)}
thinkingSuffix={resolveThinkingSuffix(model.id)}
/>
);
})}
@@ -1395,21 +1412,6 @@ export default function ProviderDetailPage() {
)}
</>
)}
{/* Thinking config */}
{/* {thinkingConfig && (
<div className="flex items-center gap-2">
<span className="text-xs text-text-muted font-medium">Thinking</span>
<select
value={thinkingMode}
onChange={(e) => handleThinkingModeChange(e.target.value)}
className="text-xs px-2 py-1 border border-border rounded-md bg-background focus:outline-none focus:border-primary"
>
{thinkingConfig.options.map((opt) => (
<option key={opt} value={opt}>{opt.charAt(0).toUpperCase() + opt.slice(1)}</option>
))}
</select>
</div>
)} */}
{/* Round Robin toggle */}
<div className="flex flex-wrap items-center gap-2">
<span className="text-xs text-text-muted font-medium">Round Robin</span>
@@ -1580,9 +1582,23 @@ export default function ProviderDetailPage() {
{/* Models */}
<Card>
<div className="mb-4 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<h2 className="text-lg font-semibold">
{"Available Models"}
</h2>
<div className="flex items-center gap-3">
<h2 className="text-lg font-semibold">
{"Available Models"}
</h2>
{providerThinkingLevels && (
<select
value={thinkingMode}
onChange={(e) => handleThinkingModeChange(e.target.value)}
title="Appends (level) suffix to copied model names"
className="rounded-md border border-border bg-background px-2 py-1 text-xs focus:border-primary focus:outline-none"
>
{providerThinkingLevels.map((opt) => (
<option key={opt} value={opt}>{`Thinking: ${opt.charAt(0).toUpperCase() + opt.slice(1)}`}</option>
))}
</select>
)}
</div>
{!isCompatible && (() => {
const allIds = [
...models,
+5 -3
View File
@@ -95,13 +95,15 @@ export const CLI_TOOLS = {
model: "ANTHROPIC_MODEL",
opusModel: "ANTHROPIC_DEFAULT_OPUS_MODEL",
sonnetModel: "ANTHROPIC_DEFAULT_SONNET_MODEL",
fableModel: "ANTHROPIC_DEFAULT_FABLE_MODEL",
haikuModel: "ANTHROPIC_DEFAULT_HAIKU_MODEL",
},
modelAliases: ["default", "sonnet", "opus", "haiku", "opusplan"],
modelAliases: ["default", "sonnet", "opus", "fable", "haiku", "opusplan"],
settingsFile: "~/.claude/settings.json",
defaultModels: [
{ id: "opus", name: "Claude Opus", alias: "opus", envKey: "ANTHROPIC_DEFAULT_OPUS_MODEL", defaultValue: "cc/claude-opus-4-6" },
{ id: "sonnet", name: "Claude Sonnet", alias: "sonnet", envKey: "ANTHROPIC_DEFAULT_SONNET_MODEL", defaultValue: "cc/claude-sonnet-4-6" },
{ id: "opus", name: "Claude Opus", alias: "opus", envKey: "ANTHROPIC_DEFAULT_OPUS_MODEL", defaultValue: "cc/claude-opus-4-8" },
{ id: "sonnet", name: "Claude Sonnet", alias: "sonnet", envKey: "ANTHROPIC_DEFAULT_SONNET_MODEL", defaultValue: "cc/claude-sonnet-5" },
{ id: "fable", name: "Claude Fable", alias: "fable", envKey: "ANTHROPIC_DEFAULT_FABLE_MODEL", defaultValue: "cc/claude-fable-5" },
{ id: "haiku", name: "Claude Haiku", alias: "haiku", envKey: "ANTHROPIC_DEFAULT_HAIKU_MODEL", defaultValue: "cc/claude-haiku-4-5-20251001" },
],
},