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:
@@ -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 100K–1M).
|
||||
// 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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user