feat(claude-code): drive auto-compact window, add a 1M-context toggle

The "Context window" dropdown wrote CLAUDE_CODE_MAX_CONTEXT_TOKENS, which
Claude Code ignores for any model it recognizes: its window resolver returns
the env value only when the id is unknown to the model table, so every
claude-* mapping kept the built-in 200K and the dropdown did nothing. It was
never the compaction threshold either.

- Replace it with CLAUDE_CODE_AUTO_COMPACT_WINDOW — the documented trigger
  (100K–1M, clamped to the model window, env beats the autoCompactWindow
  setting) — and relabel the field Auto-compact. The 1M preset becomes 700K,
  which no longer collides with the marker it depends on.
- Add a "1M context" checkbox that appends the `[1m]` marker to the
  ANTHROPIC_DEFAULT_*_MODEL envs. Claude Code assumes 200K unless the name
  carries the marker — the resolver is a plain /\[1m\]/i test on the string,
  so it applies to any id and no model lookup is involved; the user decides
  which models are worth declaring as 1M.
- Toggling rewrites the model inputs immediately, and Apply writes them
  verbatim, so a marker typed by hand is not stripped.

Rename maxContextTokens -> autoCompactWindow through the POST body and
RESET_ENV_KEYS so a reset clears the key actually written.

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
decolua
2026-09-11 00:09:15 +07:00
parent 73cb89143c
commit 17c4cc7687
2 changed files with 73 additions and 23 deletions

View File

@@ -7,19 +7,25 @@ import BaseUrlSelect from "./BaseUrlSelect";
import { rememberEndpoint } from "./cliEndpointPresets";
import ApiKeySelect from "./ApiKeySelect";
import { matchKnownEndpoint } from "./cliEndpointMatch";
import { stripModelContextMarker } from "open-sse/utils/modelMarkers.js";
const CLOUD_URL = process.env.NEXT_PUBLIC_CLOUD_URL;
// Context window presets. UI shows the round number; the value written is nudged
// down 2K to stay safely under the upstream hard cap.
// Auto-compact window presets (CLAUDE_CODE_AUTO_COMPACT_WINDOW, valid 100K1M).
// UI shows the round number; the value written is nudged down 2K to stay safely
// under the upstream hard cap.
const CONTEXT_OPTIONS = [
{ label: "Default", value: "" },
{ label: "200K", value: "198000" },
{ label: "300K", value: "298000" },
{ label: "500K", value: "498000" },
{ label: "1M", value: "998000" },
{ label: "700K", value: "698000" },
];
// Claude Code assumes a model's window is 200K unless the name carries the `[1m]`
// marker, which is why the 1M auto-compact preset only takes effect once the
// marker is applied.
export default function ClaudeToolCard({
tool,
isExpanded,
@@ -51,9 +57,28 @@ export default function ClaudeToolCard({
const [customBaseUrl, setCustomBaseUrl] = useState("");
const [ccFilterNaming, setCcFilterNaming] = useState(false);
const [exaMcpEnabled, setExaMcpEnabled] = useState(false);
const [maxContextTokens, setMaxContextTokens] = useState("");
const [autoCompactWindow, setAutoCompactWindow] = useState("");
const [oneMContext, setOneMContext] = useState(false);
const hasInitializedModels = useRef(false);
// Claude Code only string-matches the marker against the model name, so it
// applies to any id — the user decides which models are worth declaring as 1M.
// Stripping first keeps repeated toggles from stacking `[1m][1m]`.
const withContextMarker = (value, enabled) => {
const { model } = stripModelContextMarker(value);
return enabled ? `${model}[1m]` : model;
};
// Rewrite the mappings in place on toggle, so the inputs show what will be
// written without waiting for Apply.
const handleOneMContextToggle = (enabled) => {
setOneMContext(enabled);
tool.defaultModels.forEach((model) => {
const current = modelMappings[model.alias];
if (current) onModelMappingChange(model.alias, withContextMarker(current, enabled));
});
};
const currentBaseUrl = claudeStatus?.settings?.env?.ANTHROPIC_BASE_URL || "";
const getConfigStatus = () => {
@@ -80,9 +105,15 @@ export default function ClaudeToolCard({
}, [initialStatus]);
useEffect(() => {
const v = claudeStatus?.settings?.env?.CLAUDE_CODE_MAX_CONTEXT_TOKENS;
setMaxContextTokens(v || "");
}, [claudeStatus?.settings?.env?.CLAUDE_CODE_MAX_CONTEXT_TOKENS]);
const v = claudeStatus?.settings?.env?.CLAUDE_CODE_AUTO_COMPACT_WINDOW;
setAutoCompactWindow(v || "");
}, [claudeStatus?.settings?.env?.CLAUDE_CODE_AUTO_COMPACT_WINDOW]);
useEffect(() => {
const env = claudeStatus?.settings?.env;
if (!env) return;
setOneMContext(tool.defaultModels.some((model) => env[model.envKey]?.endsWith("[1m]")));
}, [claudeStatus?.settings?.env, tool.defaultModels]);
useEffect(() => {
if (isExpanded) {
@@ -124,6 +155,8 @@ export default function ClaudeToolCard({
tool.defaultModels.forEach((model) => {
if (model.envKey) {
// Kept verbatim (marker included) so the input matches what is on disk;
// withContextMarker strips before appending, so re-applying cannot double it.
const value = env[model.envKey] || model.defaultValue || "";
// Only sync initial values from file once
if (value) {
@@ -180,15 +213,17 @@ export default function ClaudeToolCard({
tool.defaultModels.forEach((model) => {
const targetModel = modelMappings[model.alias];
// Written verbatim — the input may hold a marker typed by hand, and the
// toggle already decided the marker when it was flipped.
if (targetModel && model.envKey) env[model.envKey] = targetModel;
});
if (maxContextTokens) {
env.CLAUDE_CODE_MAX_CONTEXT_TOKENS = maxContextTokens;
if (autoCompactWindow) {
env.CLAUDE_CODE_AUTO_COMPACT_WINDOW = autoCompactWindow;
}
const res = await fetch("/api/cli-tools/claude-settings", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ env, exaMcpEnabled, maxContextTokens }),
body: JSON.stringify({ env, exaMcpEnabled, autoCompactWindow }),
});
const data = await res.json();
if (res.ok) {
@@ -217,7 +252,8 @@ export default function ClaudeToolCard({
tool.defaultModels.forEach((model) => onModelMappingChange(model.alias, model.defaultValue || ""));
setSelectedApiKey("");
setExaMcpEnabled(false);
setMaxContextTokens("");
setAutoCompactWindow("");
setOneMContext(false);
} else {
setMessage({ type: "error", text: data.error || "Failed to reset settings" });
}
@@ -247,8 +283,8 @@ export default function ClaudeToolCard({
const targetModel = modelMappings[model.alias];
if (targetModel && model.envKey) env[model.envKey] = targetModel;
});
if (maxContextTokens) {
env.CLAUDE_CODE_MAX_CONTEXT_TOKENS = maxContextTokens;
if (autoCompactWindow) {
env.CLAUDE_CODE_AUTO_COMPACT_WINDOW = autoCompactWindow;
}
return [
@@ -374,17 +410,30 @@ export default function ClaudeToolCard({
</div>
))}
{/* Context Window */}
{/* Auto-compact window */}
<div className="grid grid-cols-1 gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
<span className="text-xs font-semibold text-text-main sm:text-right sm:text-sm">Context window</span>
<span className="text-xs font-semibold text-text-main sm:text-right sm:text-sm">Auto-compact</span>
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
<select value={maxContextTokens} onChange={(e) => setMaxContextTokens(e.target.value)} className="w-full min-w-0 px-2 py-2 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5">
<select value={autoCompactWindow} onChange={(e) => setAutoCompactWindow(e.target.value)} className="w-full min-w-0 px-2 py-2 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5">
{CONTEXT_OPTIONS.map((opt) => (
<option key={opt.label} value={opt.value}>{opt.label}</option>
))}
</select>
</div>
{/* 1M context */}
<div className="grid grid-cols-1 gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
<span className="text-xs font-semibold text-text-main sm:text-right sm:text-sm">1M context</span>
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
<label className="flex items-center gap-1.5 cursor-pointer select-none">
<input type="checkbox" checked={oneMContext} onChange={(e) => handleOneMContextToggle(e.target.checked)} className="w-3.5 h-3.5 accent-primary cursor-pointer" />
<span className="text-xs text-text-muted">Append [1m] to the model name</span>
<Tooltip text="Claude Code otherwise assumes a 200K window, which clamps the auto-compact window above. Applied to every mapped model — only enable it for models that really accept 1M.">
<span className="material-symbols-outlined text-text-muted text-[14px] cursor-help">info</span>
</Tooltip>
</label>
</div>
{/* CC Filter Naming */}
<div className="grid grid-cols-1 gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
<span className="text-xs font-semibold text-text-main sm:text-right sm:text-sm">Filter naming</span>

View File

@@ -123,7 +123,7 @@ export async function GET() {
// POST - Backup old fields and write new settings
export async function POST(request) {
try {
const { env, exaMcpEnabled, maxContextTokens } = await request.json();
const { env, exaMcpEnabled, autoCompactWindow } = await request.json();
if (!env || typeof env !== "object") {
return NextResponse.json(
@@ -166,12 +166,13 @@ export async function POST(request) {
},
};
// CLAUDE_CODE_MAX_CONTEXT_TOKENS — only set when a concrete value is chosen;
// "Default" removes the key so Claude Code falls back to the model's window.
if (maxContextTokens) {
newSettings.env.CLAUDE_CODE_MAX_CONTEXT_TOKENS = String(maxContextTokens);
// CLAUDE_CODE_AUTO_COMPACT_WINDOW — the token threshold that triggers
// auto-compact. Only set when a concrete value is chosen; "Default" removes
// the key so Claude Code derives the window from the model.
if (autoCompactWindow) {
newSettings.env.CLAUDE_CODE_AUTO_COMPACT_WINDOW = String(autoCompactWindow);
} else {
delete newSettings.env.CLAUDE_CODE_MAX_CONTEXT_TOKENS;
delete newSettings.env.CLAUDE_CODE_AUTO_COMPACT_WINDOW;
}
// Write new settings
@@ -203,7 +204,7 @@ const RESET_ENV_KEYS = [
"ANTHROPIC_DEFAULT_SONNET_MODEL",
"ANTHROPIC_DEFAULT_HAIKU_MODEL",
"API_TIMEOUT_MS",
"CLAUDE_CODE_MAX_CONTEXT_TOKENS",
"CLAUDE_CODE_AUTO_COMPACT_WINDOW",
];
// DELETE - Reset settings (remove env fields)