feat(ctx): add --save to /set-context-window to persist overrides (#2475)

* feat(ctx): add --save to /set-context-window to persist overrides

`/set-context-window [model] <tokens> --save` applies the session override
and also writes modelLimits["<model>"].contextWindow to user settings.json
through the existing settings writer, keeping unrelated settings and the
entry's maxOutputTokens. The output distinguishes session-only changes,
successful saves, and failed saves (the session override still applies).

After saving, the command resolves the window a restarted session would use
and reports when an env override, the route catalog, or another modelLimits
entry will take precedence over the saved value. Precedence is unchanged.

Closes #2298

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ctx): check --save values against the settings schema before writing

The session override accepts any finite integer of at least 33000 tokens,
but the settings schema only accepts safe integers for
modelLimits.*.contextWindow. Saving a larger value reported success and
wrote an entry that made the whole user settings file fail to load on
the next read.

Validate the modelLimits patch with SettingsSchema before writing it. A
rejected value now prints "Not saved: <path>: <reason>", leaves the file
unchanged, and keeps the session override.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ctx): report when invalid settings keep a saved window from loading

The settings writer keeps fields the schema rejects that are already in
the file, and the loader then ignores the whole file. --save still printed
"Saved" and blamed whichever source won without the file, for example
"fallback for unknown model ... instead of the saved value".

After writing, check whether the user settings file loads. If it does
not, say that the saved value will not apply after a restart and list the
invalid settings. Update the help text and setup docs to match.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ctx): report env settings that a --save repair makes loadable

Startup ignores an invalid user settings file, including its env block,
and the settings watcher skips --save's own write. When --save repaired
such a file, the restart check still read this session's environment and
printed a plain "Saved", although after a restart the file's env (for
example CLAUDE_CODE_OPENAI_CONTEXT_WINDOWS) applies and can take
precedence over the saved value.

Read the file's state before writing. If it did not load before and loads
now, name its env settings that this session did not load and say the
restart check does not account for them. Qualify the resolver comment and
update the help text and setup docs to match.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ctx): report a failed save for a model name settings cannot store

The settings schema drops a "__proto__" record key without an error, and
the writer's merge and the loader drop it the same way. --save checked
only that the patch validated, then printed "Saved to
modelLimits["__proto__"]" while the file kept no entry, and the restart
check reported "[object Object] tokens".

Check that the validated patch still holds the model's entry before
writing. If it does not, report "Not saved" and keep the session
override, as other failed saves do. Cover both command forms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ctx): report a failed save when the window does not reach settings

--save printed "Saved to modelLimits[...]" whenever the settings writer
returned no error. For an invalid settings file the writer merges into
the raw JSON, and when modelLimits or the model's entry is an array the
merge keeps the array and sets the new field as a named array property,
which JSON drops. The file then held no contextWindow for the model.

Read the file back after writing and report a save only when it holds
the requested contextWindow. Otherwise report "Not saved", naming the
array that blocked it, and keep the session override, as other failed
saves do. Cover both array levels in both command forms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
JATMN
2026-10-30 20:20:27 -07:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 3a8e12b804
commit beecf58f86
8 changed files with 446 additions and 26 deletions
+12
View File
@@ -661,6 +661,18 @@ addition to the `CLAUDE_CODE_OPENAI_CONTEXT_WINDOWS` /
catalogued model. For a custom or discovered model, use `modelLimits` or an
exact env entry. Use `/set-context-window <tokens>` for the current session
only.
- **Saving from the command** — `/set-context-window [model] <tokens> --save`
applies the session override and also writes
`modelLimits["<model>"].contextWindow` to your user `settings.json`, keeping
other settings and the entry's `maxOutputTokens`. The saved value follows the
precedence above after a restart; if an env override, the catalog, or another
`modelLimits` entry would take precedence, the command says so. It also
lists any other invalid settings in the file, because they make the whole
file ignored after a restart. If the save makes a previously invalid file
valid, the command names the file's `env` settings that the current session
did not load: they can apply after a restart, and the precedence check does
not account for them. If the save fails, the override still applies to the
current session.
- **Checking the active source** — `/ctx` shows a **Window source** line naming
the step that supplied the current window (for example an env override,
`modelLimits`, provider discovery, or the fallback for an unknown model).
+1 -18
View File
@@ -11,6 +11,7 @@ import type { CompactMetadata, Message } from '../../types/message.js'
import { analyzeContextUsage, type ContextData } from '../../utils/analyzeContext.js'
import { getGlobalConfig } from '../../utils/config.js'
import {
CONTEXT_WINDOW_SOURCE_LABELS,
type ContextWindowSource,
getModelMaxOutputTokens,
resolveContextWindowForModel,
@@ -166,24 +167,6 @@ export async function collectCtxData(context: CtxDataInput): Promise<{
/** Shape returned by collectCtxData, accepted by renderCtxReport. */
export type RenderInput = Awaited<ReturnType<typeof collectCtxData>>
const CONTEXT_WINDOW_SOURCE_LABELS: Record<ContextWindowSource, string> = {
'internal-env-override': 'CLAUDE_CODE_MAX_CONTEXT_TOKENS',
'session-override': 'session override (/set-context-window)',
'1m-suffix': '[1m] model suffix',
'env-exact': 'CLAUDE_CODE_OPENAI_CONTEXT_WINDOWS (exact match)',
'route-catalog': 'provider route catalog',
'env-prefix': 'CLAUDE_CODE_OPENAI_CONTEXT_WINDOWS (prefix match)',
'model-limits': 'modelLimits in settings',
discovery: 'provider model discovery',
'model-descriptor': 'built-in model metadata',
'runtime-limits': 'provider runtime limits',
fallback: 'fallback for unknown model',
'1m-beta': '1M context beta',
'1m-experiment': '1M context experiment',
'internal-model-config': 'internal model config',
default: 'default',
}
const AUTO_COMPACT_REASON_LABELS: Record<NonNullable<CompactMetadata['autoCompactReason']>, string> = {
'token-threshold': 'token threshold',
'message-count': 'message count',
+1 -1
View File
@@ -4,7 +4,7 @@ export default {
type: 'local',
name: 'set-context-window',
description: 'Set a session-scoped context window override for a model',
argumentHint: '[model] <tokens>',
argumentHint: '[model] <tokens> [--save]',
supportsNonInteractive: true,
load: () => import('./set-context-window.js'),
} satisfies Command
@@ -1,4 +1,7 @@
import { afterEach, beforeEach, expect, test } from 'bun:test'
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import {
acquireSharedMutationLock,
releaseSharedMutationLock,
@@ -8,7 +11,14 @@ import { call as setContextWindow } from './set-context-window.js'
import {
clearSessionContextWindowOverride,
getContextWindowForModel,
resolveContextWindowForModel,
} from '../../utils/context.js'
import {
getClaudeConfigHomeDirOverrideForTesting,
setClaudeConfigHomeDirForTesting,
} from '../../utils/envUtils.js'
import { resetSettingsCache } from '../../utils/settings/settingsCache.js'
import { snapshotEnv } from '../../test/envSnapshot.js'
import {
getAutoCompactThreshold,
getEffectiveContextWindowSize,
@@ -63,3 +73,247 @@ test('/set-context-window updates tokens and compaction immediately; clear resto
expect(getEffectiveContextWindowSize('claude-sonnet-4')).toBe(80_000)
expect(getAutoCompactThreshold('claude-sonnet-4')).toBe(50_000)
})
const SAVE_MODEL = 'my-private-model'
async function withCustomProviderSettings(
initialSettings: string | undefined,
run: (settingsPath: string) => Promise<void>,
): Promise<void> {
const root = mkdtempSync(join(tmpdir(), 'openclaude-set-context-window-'))
const settingsPath = join(root, 'settings.json')
if (initialSettings !== undefined) {
writeFileSync(settingsPath, initialSettings)
}
const restoreEnv = snapshotEnv(
[
'CLAUDE_CODE_USE_OPENAI',
'OPENAI_BASE_URL',
'OPENAI_API_BASE',
'OPENAI_MODEL',
'CLAUDE_CODE_OPENAI_CONTEXT_WINDOWS',
],
{ clear: true },
)
process.env.CLAUDE_CODE_USE_OPENAI = '1'
process.env.OPENAI_BASE_URL = 'https://llm.example.test/v1'
const previousConfigDir = getClaudeConfigHomeDirOverrideForTesting()
setClaudeConfigHomeDirForTesting(root)
resetSettingsCache()
try {
await run(settingsPath)
} finally {
clearSessionContextWindowOverride(SAVE_MODEL)
setClaudeConfigHomeDirForTesting(previousConfigDir)
resetSettingsCache()
restoreEnv()
rmSync(root, { recursive: true, force: true })
}
}
const saveContext = { options: { mainLoopModel: SAVE_MODEL } } as never
async function runSetContextWindow(args: string): Promise<string> {
const result = await setContextWindow(args, saveContext)
if (result.type !== 'text') throw new Error(`unexpected result type ${result.type}`)
return result.value
}
test('/set-context-window --save persists the active model window for the next session', async () => {
await withCustomProviderSettings(
JSON.stringify({
env: { KEEP_ME: '1' },
modelLimits: { [SAVE_MODEL]: { maxOutputTokens: 32_768 } },
}),
async settingsPath => {
expect(await runSetContextWindow('500000')).toContain('(session only)')
expect(JSON.parse(readFileSync(settingsPath, 'utf8')).modelLimits).toEqual({
[SAVE_MODEL]: { maxOutputTokens: 32_768 },
})
const output = await runSetContextWindow('1000000 --save')
expect(output).toContain(`Saved to modelLimits["${SAVE_MODEL}"] in ${settingsPath}.`)
expect(output).not.toContain('After a restart')
expect(JSON.parse(readFileSync(settingsPath, 'utf8'))).toEqual({
env: { KEEP_ME: '1' },
modelLimits: {
[SAVE_MODEL]: { maxOutputTokens: 32_768, contextWindow: 1_000_000 },
},
})
// A new session starts without session overrides and re-reads settings.
clearSessionContextWindowOverride()
resetSettingsCache()
expect(resolveContextWindowForModel(SAVE_MODEL)).toEqual({
contextWindow: 1_000_000,
source: 'model-limits',
})
},
)
})
test('/set-context-window --save reports when a higher-precedence source will shadow the saved value', async () => {
await withCustomProviderSettings(undefined, async settingsPath => {
process.env.CLAUDE_CODE_OPENAI_CONTEXT_WINDOWS = JSON.stringify({ [SAVE_MODEL]: 200_000 })
const output = await runSetContextWindow(`${SAVE_MODEL} 1000000 --save`)
expect(output).toContain(`Saved to modelLimits["${SAVE_MODEL}"] in ${settingsPath}.`)
expect(output).toContain(
'After a restart, CLAUDE_CODE_OPENAI_CONTEXT_WINDOWS (exact match) will set 200,000 tokens instead of the saved value.',
)
expect(JSON.parse(readFileSync(settingsPath, 'utf8')).modelLimits).toEqual({
[SAVE_MODEL]: { contextWindow: 1_000_000 },
})
expect(getContextWindowForModel(SAVE_MODEL)).toBe(1_000_000)
})
})
test.each([
['the active model', '9007199254740992 --save'],
['an explicit model', `${SAVE_MODEL} 9007199254740992 --save`],
])('/set-context-window --save does not write a window the settings schema rejects (%s)', async (_form, args) => {
const initialSettings = JSON.stringify({
env: { KEEP_ME: '1' },
modelLimits: { [SAVE_MODEL]: { maxOutputTokens: 32_768 } },
})
await withCustomProviderSettings(initialSettings, async settingsPath => {
const output = await runSetContextWindow(args)
expect(output).toContain(
`Not saved: modelLimits.${SAVE_MODEL}.contextWindow: Too big: expected int to be <=9007199254740991`,
)
expect(output).toContain('The override applies to this session only.')
expect(output).not.toContain('Saved to modelLimits')
expect(readFileSync(settingsPath, 'utf8')).toBe(initialSettings)
expect(getContextWindowForModel(SAVE_MODEL)).toBe(9_007_199_254_740_992)
})
})
test.each([
['the active model', '1000000 --save'],
['an explicit model', '__proto__ 1000000 --save'],
])('/set-context-window --save does not report a save for a model name settings cannot store (%s)', async (_form, args) => {
const initialSettings = JSON.stringify({ env: { KEEP_ME: '1' } })
await withCustomProviderSettings(initialSettings, async settingsPath => {
try {
const result = await setContextWindow(args, {
options: { mainLoopModel: '__proto__' },
} as never)
expect(result).toMatchObject({ type: 'text' })
const output = result.type === 'text' ? result.value : ''
expect(output).toContain(
'Not saved: modelLimits["__proto__"]: settings cannot store this model name',
)
expect(output).toContain('The override applies to this session only.')
expect(output).not.toContain('Saved to modelLimits')
expect(readFileSync(settingsPath, 'utf8')).toBe(initialSettings)
expect(getContextWindowForModel('__proto__')).toBe(1_000_000)
} finally {
clearSessionContextWindowOverride('__proto__')
}
})
})
test('/set-context-window --save reports when other invalid settings stop the saved file from loading', async () => {
await withCustomProviderSettings(
JSON.stringify({
env: { KEEP_ME: '1' },
modelLimits: { [SAVE_MODEL]: { maxOutputTokens: -1 } },
}),
async settingsPath => {
const output = await runSetContextWindow('1000000 --save')
expect(output).toContain(`Saved to modelLimits["${SAVE_MODEL}"] in ${settingsPath}.`)
expect(output).toContain(
`After a restart, the saved value will not apply: ${settingsPath} has invalid settings, so the whole file is ignored until they are fixed.\n modelLimits.${SAVE_MODEL}.maxOutputTokens: Number must be greater than or equal to 0`,
)
expect(output).not.toContain('instead of the saved value')
expect(JSON.parse(readFileSync(settingsPath, 'utf8'))).toEqual({
env: { KEEP_ME: '1' },
modelLimits: {
[SAVE_MODEL]: { maxOutputTokens: -1, contextWindow: 1_000_000 },
},
})
expect(getContextWindowForModel(SAVE_MODEL)).toBe(1_000_000)
},
)
})
test('/set-context-window --save reports a plain save when it replaces an invalid saved window', async () => {
await withCustomProviderSettings(
JSON.stringify({ modelLimits: { [SAVE_MODEL]: { contextWindow: -5 } } }),
async settingsPath => {
const output = await runSetContextWindow('1000000 --save')
expect(output).toContain(`Saved to modelLimits["${SAVE_MODEL}"] in ${settingsPath}.`)
expect(output).not.toContain('After a restart')
clearSessionContextWindowOverride()
resetSettingsCache()
expect(resolveContextWindowForModel(SAVE_MODEL)).toEqual({
contextWindow: 1_000_000,
source: 'model-limits',
})
},
)
})
test.each([
['the active model', '1000000 --save'],
['an explicit model', `${SAVE_MODEL} 1000000 --save`],
])('/set-context-window --save reports env settings that a repair makes loadable (%s)', async (_form, args) => {
const contextWindows = JSON.stringify({ [SAVE_MODEL]: 200_000 })
await withCustomProviderSettings(
JSON.stringify({
env: { CLAUDE_CODE_OPENAI_CONTEXT_WINDOWS: contextWindows },
modelLimits: { [SAVE_MODEL]: { contextWindow: -5 } },
}),
async settingsPath => {
// Startup ignored the invalid file, so its env was never applied.
expect(process.env.CLAUDE_CODE_OPENAI_CONTEXT_WINDOWS).toBeUndefined()
const output = await runSetContextWindow(args)
expect(output).toContain(`Saved to modelLimits["${SAVE_MODEL}"] in ${settingsPath}.`)
expect(output).toContain(
`${settingsPath} had invalid settings before this save, so this session did not load its env settings (CLAUDE_CODE_OPENAI_CONTEXT_WINDOWS). They can apply after a restart and change which window is used; this report does not account for them.`,
)
expect(JSON.parse(readFileSync(settingsPath, 'utf8'))).toEqual({
env: { CLAUDE_CODE_OPENAI_CONTEXT_WINDOWS: contextWindows },
modelLimits: { [SAVE_MODEL]: { contextWindow: 1_000_000 } },
})
expect(process.env.CLAUDE_CODE_OPENAI_CONTEXT_WINDOWS).toBeUndefined()
expect(getContextWindowForModel(SAVE_MODEL)).toBe(1_000_000)
},
)
})
test.each([
['modelLimits', 'the active model', '1000000 --save', { modelLimits: [] }],
['modelLimits', 'an explicit model', `${SAVE_MODEL} 1000000 --save`, { modelLimits: [] }],
[`modelLimits["${SAVE_MODEL}"]`, 'the active model', '1000000 --save', { modelLimits: { [SAVE_MODEL]: [] } }],
[`modelLimits["${SAVE_MODEL}"]`, 'an explicit model', `${SAVE_MODEL} 1000000 --save`, { modelLimits: { [SAVE_MODEL]: [] } }],
])('/set-context-window --save does not report a save when %s is an array (%s)', async (container, _form, args, initial) => {
const initialSettings = { env: { KEEP_ME: '1' }, ...initial }
await withCustomProviderSettings(JSON.stringify(initialSettings), async settingsPath => {
const output = await runSetContextWindow(args)
expect(output).toContain(`Not saved: ${container} in ${settingsPath} is an array`)
expect(output).toContain('The override applies to this session only.')
expect(output).not.toContain('Saved to modelLimits')
expect(JSON.parse(readFileSync(settingsPath, 'utf8'))).toEqual(initialSettings)
expect(getContextWindowForModel(SAVE_MODEL)).toBe(1_000_000)
})
})
test('/set-context-window --save keeps the session override when saving fails', async () => {
await withCustomProviderSettings('{ not json', async settingsPath => {
const output = await runSetContextWindow('1000000 --save')
expect(output).toContain(`Not saved: Invalid JSON syntax in settings file at ${settingsPath}`)
expect(output).toContain('The override applies to this session only.')
expect(readFileSync(settingsPath, 'utf8')).toBe('{ not json')
expect(getContextWindowForModel(SAVE_MODEL)).toBe(1_000_000)
})
})
@@ -1,22 +1,45 @@
import { getSdkBetas } from '../../bootstrap/state.js'
import { getBaseModelApiName } from '../../integrations/runtimeMetadata.js'
import type { LocalCommandCall } from '../../types/command.js'
import {
CONTEXT_WINDOW_SOURCE_LABELS,
resolveContextWindowForModel,
setSessionContextWindowOverride,
getSessionContextWindowOverride,
getSessionContextWindowOverrides,
} from '../../utils/context.js'
import { errorMessage } from '../../utils/errors.js'
import { readFileSync } from '../../utils/fileRead.js'
import { safeParseJSON } from '../../utils/json.js'
import {
getSettingsFilePathForSource,
parseSettingsFile,
updateSettingsForSource,
} from '../../utils/settings/settings.js'
import { SettingsSchema } from '../../utils/settings/types.js'
import { formatZodError } from '../../utils/settings/validation.js'
const HELP = `Usage: /set-context-window [model] <tokens>
const HELP = `Usage: /set-context-window [model] <tokens> [--save]
Set a session-scoped context window override. Defaults to the active model.
Examples:
/set-context-window 256000
/set-context-window gpt-4o 200000
/set-context-window my-model 1000000 --save
/set-context-window status
The override takes precedence over discovery, catalog, and provider defaults
for this session only. Use /clear-context-window to remove it.
Add --save to also write the value to modelLimits in your user settings.json
so it survives restarts. Other settings, including the model's
maxOutputTokens, are kept. After a restart the saved value follows the
modelLimits precedence below, and --save reports when another source will
take precedence over it, or when other invalid settings in the file stop it
from loading. If the save fixes a file this session could not load, it also
names the env settings in that file, which this check cannot account for.
For a permanent override (survives restart), use modelLimits in settings.json
for custom/discovered models, for example:
{ "modelLimits": { "my-model": { "contextWindow": 1000000 } } }
@@ -24,6 +47,127 @@ For a catalogued model, use an exact CLAUDE_CODE_OPENAI_CONTEXT_WINDOWS entry;
catalog values take precedence over modelLimits and prefix env entries.
See docs/advanced-setup.md (Per-model limit overrides).`
function ownProperty(value: unknown, key: string): unknown {
return value !== null && typeof value === 'object' && Object.hasOwn(value, key)
? (value as Record<string, unknown>)[key]
: undefined
}
// The writer reports success once it has written the file, not once the value
// is in it. For an invalid file it merges into the raw JSON, where an array in
// place of modelLimits or the model's entry keeps the new field only as a named
// array property, which JSON drops. Read the file back before reporting a save.
function checkContextWindowWritten(
settingsPath: string | undefined,
key: string,
tokens: number,
): Error | null {
if (!settingsPath) {
return new Error('there is no user settings file to write to')
}
let data: unknown
try {
data = safeParseJSON(readFileSync(settingsPath), false)
} catch (e) {
return new Error(`could not read ${settingsPath} back: ${errorMessage(e)}`)
}
const modelLimits = ownProperty(data, 'modelLimits')
if (Array.isArray(modelLimits)) {
return new Error(
`modelLimits in ${settingsPath} is an array, so it cannot hold model entries`,
)
}
const entry = ownProperty(modelLimits, key)
if (Array.isArray(entry)) {
return new Error(
`modelLimits["${key}"] in ${settingsPath} is an array, so it cannot hold contextWindow`,
)
}
if (ownProperty(entry, 'contextWindow') !== tokens) {
return new Error(
`modelLimits["${key}"].contextWindow was not written to ${settingsPath}`,
)
}
return null
}
function saveContextWindow(model: string, tokens: number): string {
const settingsPath = getSettingsFilePathForSource('userSettings')
const key = getBaseModelApiName(model) ?? model
const patch = { modelLimits: { [key]: { contextWindow: tokens } } }
// Startup ignores an invalid file, including its env settings, and the
// settings watcher skips this command's own write. Keep the state from
// before the write so a repair that makes the env load can be reported.
const before = settingsPath ? parseSettingsFile(settingsPath) : null
// The session override accepts values the settings schema rejects (such as
// integers above Number.MAX_SAFE_INTEGER). Writing one would stop the whole
// user settings file from loading, so check the entry before writing it.
const validation = SettingsSchema().safeParse(patch)
let error: Error | null
if (!validation.success) {
error = new Error(
formatZodError(validation.error, settingsPath ?? 'settings')
.map(issue => `${issue.path}: ${issue.message}`)
.join('; '),
)
} else if (!Object.hasOwn(validation.data.modelLimits ?? {}, key)) {
// Parsing also drops keys a settings object cannot hold, such as
// "__proto__", without an error. The writer and the loader drop the entry
// the same way, so it would never be saved or loaded.
error = new Error(
`modelLimits["${key}"]: settings cannot store this model name`,
)
} else {
error =
updateSettingsForSource('userSettings', patch).error ??
checkContextWindowWritten(settingsPath, key, tokens)
}
if (error) {
return `Not saved: ${error.message}\nThe override applies to this session only.`
}
const saved = `Saved to modelLimits["${key}"] in ${settingsPath}.`
// The writer keeps invalid fields already in the file, and the loader then
// ignores the whole file. Report that rather than naming whichever source
// wins without it as the one taking precedence.
const written = settingsPath ? parseSettingsFile(settingsPath) : null
if (written?.settings === null) {
const details = written.errors
.map(issue => `\n ${issue.path}: ${issue.message}`)
.join('')
return `${saved}\nAfter a restart, the saved value will not apply: ${settingsPath} has invalid settings, so the whole file is ignored until they are fixed.${details}`
}
// The restart check below reads this session's environment, which lacks
// env settings from a file that only became valid with this save.
const unloadedEnvKeys =
before?.settings === null
? Object.entries(written?.settings?.env ?? {})
.filter(([name, value]) => process.env[name] !== value)
.map(([name]) => name)
: []
const unloadedEnvNote =
unloadedEnvKeys.length > 0
? `\n${settingsPath} had invalid settings before this save, so this session did not load its env settings (${unloadedEnvKeys.join(', ')}). They can apply after a restart and change which window is used; this report does not account for them.`
: ''
const afterRestart = resolveContextWindowForModel(
model,
getSdkBetas(),
undefined,
{ ignoreSessionOverride: true },
)
if (
afterRestart.source === 'model-limits' &&
afterRestart.contextWindow === tokens
) {
return `${saved}${unloadedEnvNote}`
}
const shadowingSource =
afterRestart.source === 'model-limits'
? 'a higher-priority modelLimits entry'
: CONTEXT_WINDOW_SOURCE_LABELS[afterRestart.source]
return `${saved}\nAfter a restart, ${shadowingSource} will set ${afterRestart.contextWindow.toLocaleString()} tokens instead of the saved value.${unloadedEnvNote}`
}
export const call: LocalCommandCall = async (args, context) => {
const trimmed = args.trim()
@@ -42,7 +186,9 @@ export const call: LocalCommandCall = async (args, context) => {
return { type: 'text', value: `Active context window overrides:\n${lines}` }
}
const parts = trimmed.split(/\s+/)
const allParts = trimmed.split(/\s+/)
const save = allParts.includes('--save')
const parts = allParts.filter(part => part !== '--save')
let model: string
let tokensStr: string
@@ -72,8 +218,11 @@ export const call: LocalCommandCall = async (args, context) => {
const prevDisplay = previous !== undefined
? `${previous.toLocaleString()} tokens`
: 'none (using default)'
const summary = save
? `Context window for "${result.normalizedModel}" set to ${tokens.toLocaleString()} tokens for this session.\n${saveContextWindow(model, tokens)}`
: `Context window for "${result.normalizedModel}" set to ${tokens.toLocaleString()} tokens (session only).`
return {
type: 'text',
value: `Context window for "${result.normalizedModel}" set to ${tokens.toLocaleString()} tokens (session only).\nPrevious: ${prevDisplay}`,
value: `${summary}\nPrevious: ${prevDisplay}`,
}
}
+1 -1
View File
@@ -54,7 +54,7 @@ function normalizeModelApiName(
return baseModel ? baseModel.toLowerCase() : null
}
function getBaseModelApiName(value: string | undefined): string | null {
export function getBaseModelApiName(value: string | undefined): string | null {
const trimmed = value?.trim()
if (!trimmed) {
return null
+24 -2
View File
@@ -236,6 +236,24 @@ export type ContextWindowSource =
| 'internal-model-config'
| 'default'
export const CONTEXT_WINDOW_SOURCE_LABELS: Record<ContextWindowSource, string> = {
'internal-env-override': 'CLAUDE_CODE_MAX_CONTEXT_TOKENS',
'session-override': 'session override (/set-context-window)',
'1m-suffix': '[1m] model suffix',
'env-exact': 'CLAUDE_CODE_OPENAI_CONTEXT_WINDOWS (exact match)',
'route-catalog': 'provider route catalog',
'env-prefix': 'CLAUDE_CODE_OPENAI_CONTEXT_WINDOWS (prefix match)',
'model-limits': 'modelLimits in settings',
discovery: 'provider model discovery',
'model-descriptor': 'built-in model metadata',
'runtime-limits': 'provider runtime limits',
fallback: 'fallback for unknown model',
'1m-beta': '1M context beta',
'1m-experiment': '1M context experiment',
'internal-model-config': 'internal model config',
default: 'default',
}
export function getContextWindowForModel(
model: string,
betas?: string[],
@@ -247,12 +265,14 @@ export function getContextWindowForModel(
/**
* Resolve the context window together with the precedence step that supplied
* it, so diagnostics can report the real source instead of inferring it from
* the value.
* the value. `ignoreSessionOverride` skips the session override; the result
* still uses this process's environment and settings.
*/
export function resolveContextWindowForModel(
model: string,
betas?: string[],
runtimeLimits?: { contextWindow?: number },
options?: { ignoreSessionOverride?: boolean },
): { contextWindow: number; source: ContextWindowSource } {
// Allow override via environment variable (internal-only)
// This takes precedence over all other context window resolution, including 1M detection,
@@ -270,7 +290,9 @@ export function resolveContextWindowForModel(
// Session-scoped override — set via /set_context_window command.
// Takes precedence over all resolution except the internal env var above.
const sessionOverride = getSessionContextWindowOverride(model)
const sessionOverride = options?.ignoreSessionOverride
? undefined
: getSessionContextWindowOverride(model)
if (sessionOverride !== undefined) {
return { contextWindow: sessionOverride, source: 'session-override' }
}
+1 -1
View File
@@ -97,7 +97,7 @@ export const commands: SlashCommand[] = [
{ name: 'model', description: 'Set the AI model for the session', category: 'models', args: '[model]' },
{ name: 'provider', description: 'Manage API provider profiles', category: 'models' },
{ name: 'effort', description: 'Set effort level for model usage', category: 'models', args: '[low|medium|high|xhigh|max|ultracode|auto]' },
{ name: 'set-context-window', description: 'Set a session-scoped context window override for a model', category: 'models', args: '[model] <tokens>' },
{ name: 'set-context-window', description: 'Set a session-scoped context window override for a model', category: 'models', args: '[model] <tokens> [--save]' },
{ name: 'clear-context-window', description: 'Clear session-scoped context window overrides', category: 'models', args: '[model]' },
{ name: 'smartroute', description: 'Configure smart auto-routing (experimental): route simple turns to your configured simple model', category: 'models', args: '[on|off|simple <key>|strong <key>]' },
{ name: 'login', description: 'Sign in with your Anthropic account', category: 'models' },