feat(skl,cml): --context-window auto model→window probe (v5.12 B8b) [skip-docs]

Completes the deferred B8 half. `--context-window auto` now probes the
configured model and calibrates SKL/CML budgets to its real window instead
of always falling back to the conservative advisory anchor.

- lib/context-window.mjs: pure modelToContextWindow() maps known 1M-tier
  model IDs (Fable 5, Opus 4.8/4.7/4.6, Sonnet 4.6 — verified June 2026 —
  plus the explicit [1m] tier tag, dated/provider-prefixed IDs, and the
  opus/sonnet/fable aliases) to the 1M window; unknown/unconfirmed -> null
  (caller keeps the conservative anchor). resolveContextWindow() auto branch
  now probes opts.model: recognized -> auto-probed (not advisory); unknown
  or unpinned -> auto-unresolved (advisory, pre-B8b behavior).
- lib/active-model.mjs (new): resolveActiveModel() reads the model the way
  Claude Code resolves it — shell ANTHROPIC_MODEL override, then settings
  cascade local > project > user. Injectable env, hermetic under test HOME.
- scan-orchestrator: resolves the active model only when the flag is `auto`
  and threads it into resolveContextWindow; posture inherits via runAllScanners.

Default (no flag) and explicit --context-window <n> paths ignore the model
and stay byte-stable; frozen v5.0.0 + SC-5 snapshots untouched. TDD: 17 new
tests (context-window mapping/probe + active-model cascade). Suite 1296 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-06-23 22:29:12 +02:00
commit cf75249b5e
5 changed files with 308 additions and 8 deletions

View file

@ -0,0 +1,96 @@
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { resolveActiveModel } from '../../scanners/lib/active-model.mjs';
// B8b — resolveActiveModel reads the configured model so `--context-window auto`
// can probe it. Source precedence mirrors Claude Code's own resolution: the shell
// ANTHROPIC_MODEL override wins, otherwise the settings cascade (local > project >
// user). Fully file/env-injectable so it is hermetic under the test HOME.
async function withTempHome(fn) {
const home = await mkdtemp(join(tmpdir(), 'config-audit-model-home-'));
try {
return await fn(home);
} finally {
await rm(home, { recursive: true, force: true });
}
}
async function withTempProject(fn) {
const project = await mkdtemp(join(tmpdir(), 'config-audit-model-proj-'));
try {
return await fn(project);
} finally {
await rm(project, { recursive: true, force: true });
}
}
async function writeSettings(dir, file, obj) {
await mkdir(dir, { recursive: true });
await writeFile(join(dir, file), JSON.stringify(obj));
}
describe('resolveActiveModel — env override (ANTHROPIC_MODEL)', () => {
it('returns the shell ANTHROPIC_MODEL when set, ignoring settings', async () => {
await withTempHome(async (home) => {
await writeSettings(join(home, '.claude'), 'settings.json', { model: 'claude-sonnet-4-6' });
const env = { HOME: home, ANTHROPIC_MODEL: 'claude-opus-4-8[1m]' };
assert.equal(await resolveActiveModel(null, { env }), 'claude-opus-4-8[1m]');
});
});
it('an empty ANTHROPIC_MODEL is treated as unset (falls through to the cascade)', async () => {
await withTempHome(async (home) => {
await writeSettings(join(home, '.claude'), 'settings.json', { model: 'claude-sonnet-4-6' });
const env = { HOME: home, ANTHROPIC_MODEL: ' ' };
assert.equal(await resolveActiveModel(null, { env }), 'claude-sonnet-4-6');
});
});
});
describe('resolveActiveModel — settings cascade (local > project > user)', () => {
it('reads the model from user ~/.claude/settings.json', async () => {
await withTempHome(async (home) => {
await writeSettings(join(home, '.claude'), 'settings.json', { model: 'claude-opus-4-8' });
assert.equal(await resolveActiveModel(null, { env: { HOME: home } }), 'claude-opus-4-8');
});
});
it('project settings.json overrides user settings', async () => {
await withTempHome(async (home) => {
await writeSettings(join(home, '.claude'), 'settings.json', { model: 'claude-opus-4-8' });
await withTempProject(async (project) => {
await writeSettings(join(project, '.claude'), 'settings.json', { model: 'claude-sonnet-4-6' });
assert.equal(await resolveActiveModel(project, { env: { HOME: home } }), 'claude-sonnet-4-6');
});
});
});
it('settings.local.json overrides project settings (local wins)', async () => {
await withTempHome(async (home) => {
await withTempProject(async (project) => {
await writeSettings(join(project, '.claude'), 'settings.json', { model: 'claude-sonnet-4-6' });
await writeSettings(join(project, '.claude'), 'settings.local.json', { model: 'claude-opus-4-8[1m]' });
assert.equal(await resolveActiveModel(project, { env: { HOME: home } }), 'claude-opus-4-8[1m]');
});
});
});
});
describe('resolveActiveModel — nothing configured', () => {
it('returns null on a clean HOME with no model anywhere (honest advisory fallback)', async () => {
await withTempHome(async (home) => {
assert.equal(await resolveActiveModel(null, { env: { HOME: home } }), null);
});
});
it('ignores a non-string model field', async () => {
await withTempHome(async (home) => {
await writeSettings(join(home, '.claude'), 'settings.json', { model: 123 });
assert.equal(await resolveActiveModel(null, { env: { HOME: home } }), null);
});
});
});

View file

@ -3,6 +3,7 @@ import assert from 'node:assert/strict';
import {
CONTEXT_WINDOW_ANCHOR,
LARGE_CONTEXT_WINDOW,
modelToContextWindow,
resolveContextWindow,
scaleForWindow,
} from '../../scanners/lib/context-window.mjs';
@ -43,13 +44,20 @@ describe('resolveContextWindow — explicit window', () => {
});
describe('resolveContextWindow — auto / unknown downgrades to advisory', () => {
it('"auto" keeps the conservative anchor but flags advisory (no model probe yet)', () => {
it('"auto" with no model keeps the conservative anchor but flags advisory', () => {
const r = resolveContextWindow('auto');
assert.equal(r.window, CONTEXT_WINDOW_ANCHOR, 'window stays the conservative anchor');
assert.equal(r.advisory, true, 'unknown window -> advisory, not a budget breach');
assert.equal(r.source, 'auto-unresolved');
});
it('"auto" with an unrecognized model also stays advisory', () => {
const r = resolveContextWindow('auto', { model: 'totally-made-up-model' });
assert.equal(r.window, CONTEXT_WINDOW_ANCHOR);
assert.equal(r.advisory, true);
assert.equal(r.source, 'auto-unresolved');
});
it('"AUTO" is case-insensitive', () => {
assert.equal(resolveContextWindow('AUTO').advisory, true);
});
@ -74,3 +82,86 @@ describe('scaleForWindow — linear scaling off the 200k anchor', () => {
assert.equal(scaleForWindow(40_000, LARGE_CONTEXT_WINDOW), 200_000);
});
});
// B8b — model -> context-window mapping. `--context-window auto` probes the
// configured model and maps known 1M-tier model IDs to the large window so SKL/CML
// self-calibrate instead of falling back to the conservative advisory anchor.
// Verified June 2026 (platform.claude.com models overview): Fable 5, Opus
// 4.8/4.7/4.6 and Sonnet 4.6 run a 1M context window.
describe('modelToContextWindow — known 1M-tier models map to the large window', () => {
it('the explicit [1m] tier tag wins (the running session model surfaces this way)', () => {
assert.equal(modelToContextWindow('claude-opus-4-8[1m]'), LARGE_CONTEXT_WINDOW);
// Suffix wins even for a base ID we do not otherwise enumerate.
assert.equal(modelToContextWindow('claude-future-9[1m]'), LARGE_CONTEXT_WINDOW);
});
it('known 1M base IDs map to 1M', () => {
for (const id of [
'claude-fable-5',
'claude-opus-4-8',
'claude-opus-4-7',
'claude-opus-4-6',
'claude-sonnet-4-6',
]) {
assert.equal(modelToContextWindow(id), LARGE_CONTEXT_WINDOW, id);
}
});
it('tolerates dated suffixes and provider prefixes (substring match)', () => {
assert.equal(modelToContextWindow('claude-opus-4-8-20260528'), LARGE_CONTEXT_WINDOW);
assert.equal(modelToContextWindow('us.anthropic.claude-sonnet-4-6'), LARGE_CONTEXT_WINDOW);
assert.equal(modelToContextWindow('CLAUDE-OPUS-4-8'), LARGE_CONTEXT_WINDOW); // case-insensitive
});
it('short aliases that resolve to a 1M-tier model map to 1M', () => {
for (const alias of ['opus', 'sonnet', 'fable', 'opusplan']) {
assert.equal(modelToContextWindow(alias), LARGE_CONTEXT_WINDOW, alias);
}
});
});
describe('modelToContextWindow — unknown / unconfirmed models return null (conservative)', () => {
it('returns null for models whose window we do not confirm', () => {
// Haiku is intentionally NOT enumerated: keep the conservative anchor rather
// than guess. null -> caller keeps the advisory 200k anchor.
assert.equal(modelToContextWindow('claude-haiku-4-5-20251001'), null);
assert.equal(modelToContextWindow('haiku'), null);
assert.equal(modelToContextWindow('some-unknown-model'), null);
assert.equal(modelToContextWindow('default'), null);
});
it('returns null for non-string / empty input', () => {
assert.equal(modelToContextWindow(undefined), null);
assert.equal(modelToContextWindow(null), null);
assert.equal(modelToContextWindow(''), null);
assert.equal(modelToContextWindow(' '), null);
assert.equal(modelToContextWindow(42), null);
});
});
describe('resolveContextWindow — auto probes the model (B8b)', () => {
it('"auto" + a recognized 1M model calibrates to 1M, not advisory', () => {
const r = resolveContextWindow('auto', { model: 'claude-opus-4-8[1m]' });
assert.equal(r.window, LARGE_CONTEXT_WINDOW);
assert.equal(r.advisory, false, 'a confirmed window is not advisory');
assert.equal(r.source, 'auto-probed');
});
it('"auto" + a recognized base ID (no tag) also calibrates', () => {
const r = resolveContextWindow('auto', { model: 'claude-sonnet-4-6' });
assert.equal(r.window, LARGE_CONTEXT_WINDOW);
assert.equal(r.source, 'auto-probed');
});
it('the model is ignored on the explicit and default paths (byte-stable)', () => {
const explicit = resolveContextWindow('1000000', { model: 'claude-opus-4-8' });
assert.equal(explicit.source, 'explicit');
assert.equal(explicit.window, 1_000_000);
const def = resolveContextWindow(undefined, { model: 'claude-opus-4-8[1m]' });
assert.equal(def.source, 'default');
assert.equal(def.window, CONTEXT_WINDOW_ANCHOR);
assert.equal(def.advisory, false);
});
});