Skip to content

Commit bb4a9b6

Browse files
Add /context-size command to set context window for local providers
1 parent e7e6dae commit bb4a9b6

5 files changed

Lines changed: 93 additions & 7 deletions

File tree

src/provider/context-window.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,26 @@ export function contextTokensFromUsage(usage: TokenUsage | undefined): number {
2323
// Exact model-id match wins over the family heuristics below.
2424
let contextWindowRegistry: Record<string, number> = {};
2525

26+
// Provider-level overrides for local models set via /context-size.
27+
// Takes precedence over the model registry and heuristics.
28+
let providerContextOverrides: Record<string, number | undefined> = {};
29+
2630
export function setModelContextWindows(windows: Record<string, number> | undefined): void {
2731
contextWindowRegistry = windows ?? {};
2832
}
2933

34+
export function setProviderContextOverrides(overrides: Record<string, number> | undefined): void {
35+
providerContextOverrides = overrides ?? {};
36+
}
37+
38+
export function setProviderContextWindow(provider: string, tokens?: number): void {
39+
if (tokens === undefined) {
40+
providerContextOverrides[provider] = undefined;
41+
} else {
42+
providerContextOverrides[provider] = tokens;
43+
}
44+
}
45+
3046
function heuristicWindow(model: string): number {
3147
const m = model.toLowerCase();
3248
if (m.includes("gpt-5") || m.includes("codex")) return 400_000;
@@ -45,6 +61,12 @@ function heuristicWindow(model: string): number {
4561
// full identity as given, the bare model id, and `canonicalProvider/model` —
4662
// so a custom-named provider still exact-matches the registry instead of
4763
// silently missing and falling through to the heuristic.
64+
function providerFromModel(model: string): string | undefined {
65+
const colonIndex = model.indexOf(":");
66+
if (colonIndex === -1) return undefined;
67+
return model.slice(0, colonIndex);
68+
}
69+
4870
function lookupCandidates(model: string): string[] {
4971
const colonIndex = model.indexOf(":");
5072
if (colonIndex === -1) return [model];
@@ -59,12 +81,20 @@ function lookupCandidates(model: string): string[] {
5981
/** True when the registry has an entry for `model` under any known form, so a
6082
* caller can distinguish a confident lookup from the heuristic fallback. */
6183
export function hasContextWindowFor(model: string): boolean {
84+
const provider = providerFromModel(model);
85+
if (provider !== undefined && providerContextOverrides[provider] !== undefined) {
86+
return true;
87+
}
6288
return lookupCandidates(model).some(
6389
(candidate) => contextWindowRegistry[candidate] !== undefined,
6490
);
6591
}
6692

6793
export function contextWindowFor(model: string): number {
94+
const provider = providerFromModel(model);
95+
if (provider !== undefined && providerContextOverrides[provider] !== undefined) {
96+
return providerContextOverrides[provider];
97+
}
6898
for (const candidate of lookupCandidates(model)) {
6999
const exact = contextWindowRegistry[candidate];
70100
if (exact !== undefined) return exact;

src/tui/commands/built-in.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,4 +226,21 @@ export function registerBuiltInCommands(): void {
226226
};
227227
},
228228
});
229+
230+
registerCommand({
231+
name: "context-size",
232+
description: "Set context window size for the current local model (tokens)",
233+
argumentHint: "<tokens>",
234+
handler: (args, ctx) => {
235+
const tokens = Number(args.trim());
236+
if (!Number.isFinite(tokens) || tokens <= 0) {
237+
return { type: "message", text: "Usage: /context-size <positive-integer>" };
238+
}
239+
if (ctx.setContextWindow === undefined) {
240+
return { type: "message", text: "Context size is not available in this mode." };
241+
}
242+
const msg = ctx.setContextWindow(tokens);
243+
return { type: "message", text: msg };
244+
},
245+
});
229246
}

src/tui/commands/registry.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ export interface CommandContext {
2727
getSkipPermissions?: () => boolean;
2828
/** Live-flip skip-permissions and persist `/yolo` as the user-global default. */
2929
setSkipPermissions?: (value: boolean) => void;
30+
/**
31+
* Set the context window for the current provider (local models only).
32+
* Returns a user-facing status message. The implementation persists the
33+
* change to the global settings file.
34+
*/
35+
setContextWindow?: (tokens: number) => string;
3036
}
3137

3238
export type CommandResult =

src/tui/model-catalog.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -235,10 +235,11 @@ function pricingImpact(pricing: PricingCache | null, model: string): string {
235235
return `${formatPrice(price.inputPricePerToken)} / ${formatPrice(price.outputPricePerToken)} per Mtok${ratioText}.`;
236236
}
237237

238-
function whatLine(model: string): string {
239-
const reasoning = modelReasoningCapability(model);
240-
const context = contextWindowFor(model);
241-
const confident = hasContextWindowFor(model);
238+
function whatLine(modelId: string): string {
239+
const bare = modelId.includes(":") ? modelId.slice(modelId.indexOf(":") + 1) : modelId;
240+
const reasoning = modelReasoningCapability(bare);
241+
const context = contextWindowFor(modelId);
242+
const confident = hasContextWindowFor(modelId);
242243
const contextText =
243244
context > 0
244245
? `${Math.round(context / 1000)}k context${confident ? "" : " (estimated)"}`
@@ -268,15 +269,15 @@ export function describeModelCatalogOption(
268269

269270
if (option.warning !== undefined) {
270271
return {
271-
what: whatLine(model),
272+
what: whatLine(option.id),
272273
impact:
273274
"A Go model reached over the Zen path. Billed as Zen credits, not your Go subscription.",
274275
tone: "consequence",
275276
};
276277
}
277278

278279
return {
279-
what: whatLine(model),
280+
what: whatLine(option.id),
280281
impact: pricingImpact(pricing, model),
281282
tone: "plain",
282283
};

src/tui/runner.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,11 @@ import {
133133
maskContextMeterWhenNoTurns,
134134
type CostSummary,
135135
} from "../cost/cost-summary.js";
136-
import { contextTokensFromUsage } from "../provider/context-window.js";
136+
import {
137+
contextTokensFromUsage,
138+
setProviderContextWindow,
139+
setProviderContextOverrides,
140+
} from "../provider/context-window.js";
137141
import {
138142
advertisedToolNamesForSessionMode,
139143
advertisedTools,
@@ -587,6 +591,17 @@ export function setUpCommandRegistry(
587591

588592
export async function runTUI(initialConfig: Config): Promise<number> {
589593
let config = initialConfig;
594+
// Seed provider context-window overrides from persisted settings so the
595+
// model picker shows the user-set value after restart.
596+
if (config.settings?.providers) {
597+
const overrides: Record<string, number> = {};
598+
for (const [providerName, p] of Object.entries(config.settings.providers)) {
599+
if (typeof p.contextWindow === "number" && p.contextWindow > 0) {
600+
overrides[providerName] = p.contextWindow;
601+
}
602+
}
603+
setProviderContextOverrides(overrides);
604+
}
590605
const inferenceDeps = await createInferenceDependencies();
591606

592607
// Auto-discover plugins from the repo's plugins/ directory and user plugin
@@ -2113,6 +2128,23 @@ export async function runTUI(initialConfig: Config): Promise<number> {
21132128
beginFeedbackCapture: () => {
21142129
armFeedbackCapture();
21152130
},
2131+
setContextWindow: (tokens: number) => {
2132+
const providerName = config.providerName;
2133+
const providerSettings = config.settings?.providers?.[providerName];
2134+
const isLocal = config.keyless || providerSettings?.keyless === true;
2135+
if (!isLocal) {
2136+
return "Context size can only be set for local models.";
2137+
}
2138+
void persistGlobalSettings("contextWindow", (base) => {
2139+
const prov = base.providers[providerName];
2140+
if (prov) {
2141+
prov.contextWindow = tokens;
2142+
}
2143+
return base;
2144+
});
2145+
setProviderContextWindow(providerName, tokens);
2146+
return `Context window set to ${tokens} tokens for ${providerName}.`;
2147+
},
21162148
};
21172149

21182150
// Routed through the shell's notice path rather than straight into the

0 commit comments

Comments
 (0)