Skip to content

Commit 307d456

Browse files
committed
refactor(cli): read context window from extension state instead of resolving it locally
The CLI resolved the context window itself by looking up the current model in routerModels via getModelId(). That only worked for router providers (static providers such as Anthropic, Bedrock and Gemini always fell back to 200K) and tied the CLI to provider model-ID logic. The extension now reports the active task's resolved context window as ExtensionState.contextWindow, read from the task's API handler in getStateToPostToWebview(). The CLI stores that value and falls back to the 200K default when it is absent, and no longer tracks routerModels or apiConfiguration for this purpose.
1 parent 70eba96 commit 307d456

9 files changed

Lines changed: 85 additions & 225 deletions

File tree

Lines changed: 9 additions & 152 deletions
Original file line numberDiff line numberDiff line change
@@ -1,160 +1,17 @@
1-
import { providerIdentifiers, retiredProviderIdentifiers, type ProviderSettings } from "@roo-code/types"
2-
31
import { DEFAULT_CONTEXT_WINDOW, getContextWindow } from "../context-window.js"
42

53
describe("getContextWindow", () => {
6-
it.each([
7-
[providerIdentifiers.openrouter, "openRouterModelId"],
8-
[providerIdentifiers.ollama, "ollamaModelId"],
9-
[providerIdentifiers.lmstudio, "lmStudioModelId"],
10-
[providerIdentifiers.openai, "openAiModelId"],
11-
[providerIdentifiers.requesty, "requestyModelId"],
12-
[providerIdentifiers.unbound, "unboundModelId"],
13-
[providerIdentifiers.litellm, "litellmModelId"],
14-
[providerIdentifiers.vercelAiGateway, "vercelAiGatewayModelId"],
15-
[providerIdentifiers.opencodeGo, "opencodeGoModelId"],
16-
[providerIdentifiers.kenari, "kenariModelId"],
17-
[providerIdentifiers.ioIntelligence, "ioIntelligenceModelId"],
18-
[providerIdentifiers.nanogpt, "nanoGptModelId"],
19-
[providerIdentifiers.zooGateway, "zooGatewayModelId"],
20-
] as const)("uses the provider-specific model field for %s", (provider, modelField) => {
21-
const config = { apiProvider: provider, [modelField]: "selected-model" } as ProviderSettings
22-
const routerModels = { [provider]: { "selected-model": { contextWindow: 123_456 } } }
23-
24-
expect(getContextWindow(routerModels, config)).toBe(123_456)
4+
it.each([1, 32_000, 200_000, 1_048_576])("returns the window reported by the extension (%d)", (contextWindow) => {
5+
expect(getContextWindow(contextWindow)).toBe(contextWindow)
256
})
267

278
it.each([
28-
providerIdentifiers.anthropic,
29-
providerIdentifiers.bedrock,
30-
providerIdentifiers.deepseek,
31-
providerIdentifiers.fireworks,
32-
providerIdentifiers.gemini,
33-
providerIdentifiers.mistral,
34-
providerIdentifiers.openaiNative,
35-
providerIdentifiers.vertex,
36-
providerIdentifiers.xai,
37-
] as const)("uses the shared apiModelId field for %s", (provider) => {
38-
const config: ProviderSettings = { apiProvider: provider, apiModelId: "selected-model" }
39-
const routerModels = { [provider]: { "selected-model": { contextWindow: 64_000 } } }
40-
41-
expect(getContextWindow(routerModels, config)).toBe(64_000)
42-
})
43-
44-
it("ignores another provider's model field", () => {
45-
const config: ProviderSettings = {
46-
apiProvider: providerIdentifiers.ollama,
47-
openRouterModelId: "openrouter-model",
48-
ollamaModelId: "ollama-model",
49-
}
50-
const routerModels = {
51-
[providerIdentifiers.ollama]: { "ollama-model": { contextWindow: 32_000 } },
52-
}
53-
54-
expect(getContextWindow(routerModels, config)).toBe(32_000)
55-
})
56-
57-
describe("VS Code LM", () => {
58-
const provider = providerIdentifiers.vscodeLm
59-
const routerModels = { [provider]: { "selected-model": { contextWindow: 123_456 } } }
60-
61-
it("takes the model from the saved selector", () => {
62-
const config: ProviderSettings = {
63-
apiProvider: provider,
64-
vsCodeLmModelSelector: { vendor: "copilot", id: "selected-model" },
65-
}
66-
67-
expect(getContextWindow(routerModels, config)).toBe(123_456)
68-
})
69-
70-
it("ignores apiModelId, which the selector replaces", () => {
71-
const config: ProviderSettings = {
72-
apiProvider: provider,
73-
apiModelId: "other-model",
74-
vsCodeLmModelSelector: { vendor: "copilot", id: "selected-model" },
75-
}
76-
77-
expect(getContextWindow(routerModels, config)).toBe(123_456)
78-
})
79-
})
80-
81-
describe("GitHub Copilot", () => {
82-
const provider = providerIdentifiers.githubCopilot
83-
const routerModels = { [provider]: { "selected-model": { contextWindow: 123_456 } } }
84-
85-
it("takes the model from the saved selector, where Copilot keeps it", () => {
86-
const config: ProviderSettings = {
87-
apiProvider: provider,
88-
vsCodeLmModelSelector: { vendor: "copilot", id: "selected-model" },
89-
}
90-
91-
expect(getContextWindow(routerModels, config)).toBe(123_456)
92-
})
93-
94-
it("ignores apiModelId, which Copilot profiles do not use", () => {
95-
const config: ProviderSettings = {
96-
apiProvider: provider,
97-
apiModelId: "other-model",
98-
vsCodeLmModelSelector: { vendor: "copilot", id: "selected-model" },
99-
}
100-
101-
expect(getContextWindow(routerModels, config)).toBe(123_456)
102-
})
103-
104-
it.each([
105-
["no selector", undefined],
106-
["a selector that names a family but no id", { vendor: "copilot", family: "gpt-5.5" }],
107-
])("returns the default for %s", (_label, vsCodeLmModelSelector) => {
108-
const config: ProviderSettings = { apiProvider: provider, vsCodeLmModelSelector }
109-
110-
expect(getContextWindow(routerModels, config)).toBe(DEFAULT_CONTEXT_WINDOW)
111-
})
112-
113-
it("returns the default when the selected model is not in the list", () => {
114-
const config: ProviderSettings = {
115-
apiProvider: provider,
116-
vsCodeLmModelSelector: { vendor: "copilot", id: "unlisted-model" },
117-
}
118-
119-
expect(getContextWindow(routerModels, config)).toBe(DEFAULT_CONTEXT_WINDOW)
120-
})
121-
})
122-
123-
it("returns the default when the selected model is unavailable", () => {
124-
const routerModels = { [providerIdentifiers.openrouter]: { "other-model": { contextWindow: 1_000 } } }
125-
const config: ProviderSettings = {
126-
apiProvider: providerIdentifiers.openrouter,
127-
openRouterModelId: "missing-model",
128-
}
129-
130-
expect(getContextWindow(routerModels, config)).toBe(DEFAULT_CONTEXT_WINDOW)
131-
})
132-
133-
it("returns the default when no provider is selected", () => {
134-
expect(getContextWindow({}, {})).toBe(DEFAULT_CONTEXT_WINDOW)
135-
})
136-
137-
it("returns the default when router models are unavailable for a retired provider", () => {
138-
const config: ProviderSettings = {
139-
apiProvider: retiredProviderIdentifiers.groq,
140-
apiModelId: "selected-model",
141-
}
142-
143-
expect(getContextWindow({}, config)).toBe(DEFAULT_CONTEXT_WINDOW)
144-
})
145-
146-
it("resolves the model for a retired provider instead of throwing", () => {
147-
const config: ProviderSettings = {
148-
apiProvider: retiredProviderIdentifiers.groq,
149-
apiModelId: "selected-model",
150-
}
151-
const routerModels = { [retiredProviderIdentifiers.groq]: { "selected-model": { contextWindow: 48_000 } } }
152-
153-
expect(getContextWindow(routerModels, config)).toBe(48_000)
154-
})
155-
156-
it("returns the default when router models or configuration is missing", () => {
157-
expect(getContextWindow(null, { apiProvider: providerIdentifiers.anthropic })).toBe(DEFAULT_CONTEXT_WINDOW)
158-
expect(getContextWindow({}, null)).toBe(DEFAULT_CONTEXT_WINDOW)
9+
["undefined", undefined],
10+
["null", null],
11+
["zero", 0],
12+
["negative", -1],
13+
["NaN", Number.NaN],
14+
])("returns the default when the reported window is %s", (_label, contextWindow) => {
15+
expect(getContextWindow(contextWindow)).toBe(DEFAULT_CONTEXT_WINDOW)
15916
})
16017
})
Lines changed: 8 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,16 @@
1-
import { getModelId, type ProviderSettings } from "@roo-code/types"
2-
3-
import type { RouterModels } from "@/ui/store.js"
4-
51
const DEFAULT_CONTEXT_WINDOW = 200_000
62

73
/**
8-
* Looks up the context window size for the current model from routerModels.
4+
* Resolves the context window to display for the current model.
95
*
10-
* @param routerModels - The router models data containing model info per provider
11-
* @param apiConfiguration - The current API configuration with provider and model ID
12-
* @returns The context window size, or DEFAULT_CONTEXT_WINDOW (200K) if not found
6+
* The extension resolves the model through its provider handler and posts the result as
7+
* `ExtensionState.contextWindow`, so this covers static and router providers alike.
8+
*
9+
* @param contextWindow - The context window reported by the extension state, if any
10+
* @returns The reported context window, or DEFAULT_CONTEXT_WINDOW (200K) when it is missing or invalid
1311
*/
14-
export function getContextWindow(routerModels: RouterModels | null, apiConfiguration: ProviderSettings | null): number {
15-
if (!routerModels || !apiConfiguration) {
16-
return DEFAULT_CONTEXT_WINDOW
17-
}
18-
19-
const provider = apiConfiguration.apiProvider
20-
const modelId = getModelId(apiConfiguration)
21-
22-
if (!provider || !modelId) {
23-
return DEFAULT_CONTEXT_WINDOW
24-
}
25-
26-
const providerModels = routerModels[provider]
27-
const modelInfo = providerModels?.[modelId]
28-
29-
return modelInfo?.contextWindow ?? DEFAULT_CONTEXT_WINDOW
12+
export function getContextWindow(contextWindow: number | null | undefined): number {
13+
return typeof contextWindow === "number" && contextWindow > 0 ? contextWindow : DEFAULT_CONTEXT_WINDOW
3014
}
3115

3216
export { DEFAULT_CONTEXT_WINDOW }

‎apps/cli/src/ui/App.tsx‎

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,7 @@ function AppInner({ createExtensionHost, ...extensionHostOptions }: TUIAppProps)
106106
taskHistory,
107107
currentMode,
108108
tokenUsage,
109-
routerModels,
110-
apiConfiguration,
109+
contextWindow: reportedContextWindow,
111110
currentTodos,
112111
} = useCLIStore()
113112

@@ -122,10 +121,8 @@ function AppInner({ createExtensionHost, ...extensionHostOptions }: TUIAppProps)
122121
setIsTransitioningToCustomInput,
123122
} = useUIStateStore()
124123

125-
// Compute context window from router models and API configuration
126-
const contextWindow = useMemo(() => {
127-
return getContextWindow(routerModels, apiConfiguration)
128-
}, [routerModels, apiConfiguration])
124+
// Context window reported by the extension, falling back to the default
125+
const contextWindow = getContextWindow(reportedContextWindow)
129126

130127
// eslint-disable-next-line @typescript-eslint/no-explicit-any
131128
const autocompleteRef = useRef<AutocompleteInputHandle<any>>(null)

‎apps/cli/src/ui/__tests__/store.test.ts‎

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
import { providerIdentifiers } from "@roo-code/types"
2-
import { RooCodeSettings } from "@roo-code/types"
3-
41
import { useCLIStore } from "../store.js"
52

63
describe("useCLIStore", () => {
@@ -159,25 +156,12 @@ describe("useCLIStore", () => {
159156
expect(useCLIStore.getState().currentMode).toBe("architect")
160157
})
161158

162-
it("should PRESERVE routerModels", () => {
163-
const models = { openai: { "gpt-4": { contextWindow: 128000 } } }
164-
useCLIStore.getState().setRouterModels(models)
165-
166-
useCLIStore.getState().resetForTaskSwitch()
167-
168-
expect(useCLIStore.getState().routerModels).toEqual(models)
169-
})
170-
171-
it("should PRESERVE apiConfiguration", () => {
172-
const config: RooCodeSettings = { apiProvider: providerIdentifiers.openai, apiModelId: "gpt-4" }
173-
174-
useCLIStore
175-
.getState()
176-
.setApiConfiguration(config as ReturnType<typeof useCLIStore.getState>["apiConfiguration"])
159+
it("should PRESERVE contextWindow", () => {
160+
useCLIStore.getState().setContextWindow(128_000)
177161

178162
useCLIStore.getState().resetForTaskSwitch()
179163

180-
expect(useCLIStore.getState().apiConfiguration).toEqual(config)
164+
expect(useCLIStore.getState().contextWindow).toBe(128_000)
181165
})
182166
})
183167

‎apps/cli/src/ui/hooks/useMessageHandlers.ts‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export function useMessageHandlers({ nonInteractive }: UseMessageHandlersOptions
4040
setAvailableModes,
4141
setCurrentMode,
4242
setTokenUsage,
43-
setRouterModels,
43+
setContextWindow,
4444
setTaskHistory,
4545
currentTodos,
4646
setTodos,
@@ -315,6 +315,10 @@ export function useMessageHandlers({ nonInteractive }: UseMessageHandlersOptions
315315
setCurrentMode(newMode)
316316
}
317317

318+
// The extension resolves the current task's model and reports its context window.
319+
// When no task is active it's undefined (nothing uses the value when no task is active)
320+
setContextWindow(state.contextWindow ?? null)
321+
318322
// Extract and update task history from state
319323
const newTaskHistory = state.taskHistory
320324

@@ -382,10 +386,6 @@ export function useMessageHandlers({ nonInteractive }: UseMessageHandlersOptions
382386
setAllSlashCommands((msg.commands as SlashCommandResult[]) || [])
383387
} else if (msg.type === "modes") {
384388
setAvailableModes((msg.modes as ModeResult[]) || [])
385-
} else if (msg.type === "routerModels") {
386-
if (msg.routerModels) {
387-
setRouterModels(msg.routerModels)
388-
}
389389
}
390390
},
391391
[
@@ -396,7 +396,7 @@ export function useMessageHandlers({ nonInteractive }: UseMessageHandlersOptions
396396
setAvailableModes,
397397
setCurrentMode,
398398
setTokenUsage,
399-
setRouterModels,
399+
setContextWindow,
400400
setTaskHistory,
401401
],
402402
)

‎apps/cli/src/ui/store.ts‎

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { create } from "zustand"
22

3-
import type { TokenUsage, ProviderSettings, TodoItem } from "@roo-code/types"
3+
import type { TokenUsage, TodoItem } from "@roo-code/types"
44

55
import type { TUIMessage, PendingAsk, TaskHistoryItem } from "./types.js"
66
import type { FileResult, SlashCommandResult, ModeResult } from "./components/autocomplete/index.js"
@@ -37,12 +37,6 @@ interface PendingStreamUpdate {
3737
const pendingStreamUpdates: Map<string, PendingStreamUpdate> = new Map()
3838
let streamingDebounceTimer: ReturnType<typeof setTimeout> | null = null
3939

40-
/**
41-
* RouterModels type for context window lookup.
42-
* Simplified version - we only need contextWindow from ModelInfo.
43-
*/
44-
export type RouterModels = Record<string, Record<string, { contextWindow?: number }>>
45-
4640
/**
4741
* CLI application state.
4842
*
@@ -82,9 +76,8 @@ interface CLIState {
8276
// Token usage metrics (from getApiMetrics)
8377
tokenUsage: TokenUsage | null
8478

85-
// Model info for context window lookup
86-
routerModels: RouterModels | null
87-
apiConfiguration: ProviderSettings | null
79+
// Context window of the current model, as resolved by the extension
80+
contextWindow: number | null
8881

8982
// Todo list tracking
9083
currentTodos: TodoItem[]
@@ -124,8 +117,7 @@ interface CLIActions {
124117

125118
// Metrics actions
126119
setTokenUsage: (usage: TokenUsage | null) => void
127-
setRouterModels: (models: RouterModels | null) => void
128-
setApiConfiguration: (config: ProviderSettings | null) => void
120+
setContextWindow: (contextWindow: number | null) => void
129121

130122
// Todo actions
131123
setTodos: (todos: TodoItem[]) => void
@@ -146,8 +138,7 @@ const initialState: CLIState = {
146138
currentTaskId: null,
147139
currentMode: null,
148140
tokenUsage: null,
149-
routerModels: null,
150-
apiConfiguration: null,
141+
contextWindow: null,
151142
currentTodos: [],
152143
previousTodos: [],
153144
}
@@ -273,8 +264,7 @@ export const useCLIStore = create<CLIState & CLIActions>((set, get) => ({
273264
allSlashCommands: state.allSlashCommands,
274265
fileSearchResults: state.fileSearchResults,
275266
currentMode: state.currentMode,
276-
routerModels: state.routerModels,
277-
apiConfiguration: state.apiConfiguration,
267+
contextWindow: state.contextWindow,
278268
})),
279269
setIsResumingTask: (isResuming) => set({ isResumingTask: isResuming }),
280270
// Use shallow equality to prevent unnecessary re-renders when array content is the same
@@ -289,7 +279,6 @@ export const useCLIStore = create<CLIState & CLIActions>((set, get) => ({
289279
setCurrentTaskId: (taskId) => set({ currentTaskId: taskId }),
290280
setCurrentMode: (mode) => set({ currentMode: mode }),
291281
setTokenUsage: (usage) => set({ tokenUsage: usage }),
292-
setRouterModels: (models) => set({ routerModels: models }),
293-
setApiConfiguration: (config) => set({ apiConfiguration: config }),
282+
setContextWindow: (contextWindow) => set({ contextWindow }),
294283
setTodos: (todos) => set((state) => ({ previousTodos: state.currentTodos, currentTodos: todos })),
295284
}))

‎packages/types/src/vscode-extension-host.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,12 @@ export type ExtensionState = Pick<
354354
currentTaskItem?: HistoryItem
355355
currentTaskTodos?: TodoItem[] // Initial todos for the current task
356356
apiConfiguration: ProviderSettings
357+
/**
358+
* Context window (in tokens) of the current task's resolved model, as computed by the
359+
* extension's provider handler. Covers static and router providers alike, so consumers
360+
* (e.g. the CLI) need no provider-specific model lookup. Undefined when no task is active.
361+
*/
362+
contextWindow?: number
357363
uriScheme?: string
358364
shouldShowAnnouncement: boolean
359365

0 commit comments

Comments
 (0)