Skip to content

Commit 29781ca

Browse files
committed
feat: Vertex Claude Sonnet 4 1M context: model, pricing, settings, UI, and test support
1 parent 7cd6520 commit 29781ca

File tree

25 files changed

+258
-25
lines changed

25 files changed

+258
-25
lines changed

packages/types/src/provider-settings.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ const vertexSchema = apiModelIdProviderModelSchema.extend({
170170
vertexRegion: z.string().optional(),
171171
enableUrlContext: z.boolean().optional(),
172172
enableGrounding: z.boolean().optional(),
173+
vertex1MContext: z.boolean().optional(), // Enable 1M context for Claude Sonnet 4 on Vertex AI
173174
})
174175

175176
const openAiSchema = baseProviderSettingsSchema.extend({

packages/types/src/providers/vertex.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ export type VertexModelId = keyof typeof vertexModels
55

66
export const vertexDefaultModelId: VertexModelId = "claude-sonnet-4@20250514"
77

8+
export const ANTHROPIC_VERTEX_1M_CONTEXT_MODEL_ID: VertexModelId = "claude-sonnet-4@20250514"
9+
810
export const vertexModels = {
911
"gemini-2.5-flash-preview-05-20:thinking": {
1012
maxTokens: 65_535,
@@ -174,6 +176,15 @@ export const vertexModels = {
174176
cacheWritesPrice: 3.75,
175177
cacheReadsPrice: 0.3,
176178
supportsReasoningBudget: true,
179+
tiers: [
180+
{
181+
contextWindow: 1_000_000,
182+
inputPrice: 3.0,
183+
outputPrice: 15.0,
184+
cacheWritesPrice: 3.75,
185+
cacheReadsPrice: 0.3,
186+
},
187+
],
177188
},
178189
"claude-opus-4-1@20250805": {
179190
maxTokens: 8192,

src/api/providers/__tests__/anthropic-vertex.spec.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -578,6 +578,65 @@ describe("VertexHandler", () => {
578578
}
579579

580580
const mockCreate = vitest.fn().mockResolvedValue(asyncIterator)
581+
582+
describe("vertex1MContext logic", () => {
583+
it("should use 1M context window when vertex1MContext is enabled for Claude Sonnet 4", () => {
584+
const handler = new AnthropicVertexHandler({
585+
apiModelId: "claude-sonnet-4@20250514",
586+
vertexProjectId: "test-project",
587+
vertexRegion: "us-east5",
588+
vertex1MContext: true,
589+
})
590+
const model = handler.getModel()
591+
expect(model.info.contextWindow).toBe(1_000_000)
592+
})
593+
594+
it("should use default context window for other models or when vertex1MContext is not set", () => {
595+
const handlerDefault = new AnthropicVertexHandler({
596+
apiModelId: "claude-3-opus@20240229",
597+
vertexProjectId: "test-project",
598+
vertexRegion: "us-east5",
599+
})
600+
const modelDefault = handlerDefault.getModel()
601+
expect(modelDefault.info.contextWindow).toBe(200_000)
602+
603+
const handlerNo1M = new AnthropicVertexHandler({
604+
apiModelId: "claude-sonnet-4@20250514",
605+
vertexProjectId: "test-project",
606+
vertexRegion: "us-east5",
607+
})
608+
const modelNo1M = handlerNo1M.getModel()
609+
expect(modelNo1M.info.contextWindow).toBe(200_000)
610+
})
611+
612+
it("accepts a prompt > 200k tokens when 1M context enabled and does not trigger reduction/compression", async () => {
613+
const longPrompt = "hello ".repeat(250_000).trim() // ~250k "tokens"
614+
const handler = new AnthropicVertexHandler({
615+
apiModelId: "claude-sonnet-4@20250514",
616+
vertexProjectId: "test-project",
617+
vertexRegion: "us-east5",
618+
vertex1MContext: true,
619+
})
620+
const { info: modelInfo } = handler.getModel()
621+
expect(modelInfo.contextWindow).toBe(1_000_000)
622+
const mockCreate = vitest.fn().mockResolvedValue({
623+
content: [{ type: "text", text: "ok" }],
624+
})
625+
;(handler as any).client = { messages: { create: mockCreate } }
626+
const result = await handler.completePrompt(longPrompt)
627+
expect(result).toBe("ok")
628+
expect(mockCreate).toHaveBeenCalledWith(
629+
expect.objectContaining({
630+
messages: [
631+
{
632+
role: "user",
633+
content: [{ type: "text", text: longPrompt, cache_control: { type: "ephemeral" } }],
634+
},
635+
],
636+
}),
637+
)
638+
})
639+
})
581640
;(handler["client"].messages as any).create = mockCreate
582641

583642
const stream = handler.createMessage(systemPrompt, mockMessages)

src/api/providers/anthropic-vertex.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import fs from "fs"
12
import { Anthropic } from "@anthropic-ai/sdk"
23
import { AnthropicVertex } from "@anthropic-ai/vertex-sdk"
34
import { GoogleAuth, JWTInput } from "google-auth-library"
@@ -8,6 +9,7 @@ import {
89
vertexDefaultModelId,
910
vertexModels,
1011
ANTHROPIC_DEFAULT_MAX_TOKENS,
12+
ANTHROPIC_VERTEX_1M_CONTEXT_MODEL_ID,
1113
} from "@roo-code/types"
1214

1315
import { ApiHandlerOptions } from "../../shared/api"
@@ -35,12 +37,20 @@ export class AnthropicVertexHandler extends BaseProvider implements SingleComple
3537
const region = this.options.vertexRegion ?? "us-east5"
3638

3739
if (this.options.vertexJsonCredentials) {
40+
let credentials = this.options.vertexJsonCredentials
41+
try {
42+
if (fs.existsSync(credentials)) {
43+
credentials = fs.readFileSync(credentials, "utf-8")
44+
}
45+
} catch (error) {
46+
console.error("Error reading Vertex credentials file:", error)
47+
}
3848
this.client = new AnthropicVertex({
3949
projectId,
4050
region,
4151
googleAuth: new GoogleAuth({
4252
scopes: ["https://www.googleapis.com/auth/cloud-platform"],
43-
credentials: safeJsonParse<JWTInput>(this.options.vertexJsonCredentials, undefined),
53+
credentials: safeJsonParse<JWTInput>(credentials, undefined),
4454
}),
4555
})
4656
} else if (this.options.vertexKeyFile) {
@@ -165,9 +175,20 @@ export class AnthropicVertexHandler extends BaseProvider implements SingleComple
165175
getModel() {
166176
const modelId = this.options.apiModelId
167177
let id = modelId && modelId in vertexModels ? (modelId as VertexModelId) : vertexDefaultModelId
168-
const info: ModelInfo = vertexModels[id]
178+
const info: ModelInfo = { ...vertexModels[id] }
169179
const params = getModelParams({ format: "anthropic", modelId: id, model: info, settings: this.options })
170180

181+
if (id === ANTHROPIC_VERTEX_1M_CONTEXT_MODEL_ID && this.options.vertex1MContext) {
182+
info.contextWindow = 1_000_000
183+
const tier = info.tiers?.[0]
184+
if (tier) {
185+
info.inputPrice = tier.inputPrice
186+
info.outputPrice = tier.outputPrice
187+
info.cacheWritesPrice = tier.cacheWritesPrice
188+
info.cacheReadsPrice = tier.cacheReadsPrice
189+
}
190+
}
191+
171192
// The `:thinking` suffix indicates that the model is a "Hybrid"
172193
// reasoning model and that reasoning is required to be enabled.
173194
// The actual model ID honored by Anthropic's API does not have this

src/core/config/ProviderSettingsManager.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,10 @@ export class ProviderSettingsManager {
221221
if (apiConfig.fuzzyMatchThreshold === undefined) {
222222
apiConfig.fuzzyMatchThreshold = fuzzyMatchThreshold
223223
}
224+
// Ensure new vertex1MContext field is present for Vertex provider configs
225+
if (apiConfig.apiProvider === "vertex" && apiConfig.vertex1MContext === undefined) {
226+
apiConfig.vertex1MContext = false
227+
}
224228
}
225229
} catch (error) {
226230
console.error(`[MigrateDiffSettings] Failed to migrate diff settings:`, error)

webview-ui/src/components/settings/providers/Vertex.tsx

Lines changed: 47 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { useCallback } from "react"
22
import { Checkbox } from "vscrui"
33
import { VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
44

5-
import { type ProviderSettings, VERTEX_REGIONS } from "@roo-code/types"
5+
import { ANTHROPIC_VERTEX_1M_CONTEXT_MODEL_ID, type ProviderSettings, VERTEX_REGIONS } from "@roo-code/types"
66

77
import { useAppTranslation } from "@src/i18n/TranslationContext"
88
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@src/components/ui"
@@ -94,29 +94,54 @@ export const Vertex = ({ apiConfiguration, setApiConfigurationField, fromWelcome
9494
</Select>
9595
</div>
9696

97-
{!fromWelcomeView && apiConfiguration.apiModelId?.startsWith("gemini") && (
98-
<div className="mt-6">
99-
<Checkbox
100-
data-testid="checkbox-url-context"
101-
checked={!!apiConfiguration.enableUrlContext}
102-
onChange={(checked: boolean) => setApiConfigurationField("enableUrlContext", checked)}>
103-
{t("settings:providers.geminiParameters.urlContext.title")}
104-
</Checkbox>
105-
<div className="text-sm text-vscode-descriptionForeground mb-3 mt-1.5">
106-
{t("settings:providers.geminiParameters.urlContext.description")}
107-
</div>
97+
{!fromWelcomeView &&
98+
(apiConfiguration.apiModelId?.startsWith("gemini") ||
99+
apiConfiguration.apiModelId === ANTHROPIC_VERTEX_1M_CONTEXT_MODEL_ID) && (
100+
<div className="mt-6">
101+
{apiConfiguration.apiModelId?.startsWith("gemini") && (
102+
<>
103+
<Checkbox
104+
data-testid="checkbox-url-context"
105+
checked={!!apiConfiguration.enableUrlContext}
106+
onChange={(checked: boolean) =>
107+
setApiConfigurationField("enableUrlContext", checked)
108+
}>
109+
{t("settings:providers.geminiParameters.urlContext.title")}
110+
</Checkbox>
111+
<div className="text-sm text-vscode-descriptionForeground mb-3 mt-1.5">
112+
{t("settings:providers.geminiParameters.urlContext.description")}
113+
</div>
108114

109-
<Checkbox
110-
data-testid="checkbox-grounding-search"
111-
checked={!!apiConfiguration.enableGrounding}
112-
onChange={(checked: boolean) => setApiConfigurationField("enableGrounding", checked)}>
113-
{t("settings:providers.geminiParameters.groundingSearch.title")}
114-
</Checkbox>
115-
<div className="text-sm text-vscode-descriptionForeground mb-3 mt-1.5">
116-
{t("settings:providers.geminiParameters.groundingSearch.description")}
115+
<Checkbox
116+
data-testid="checkbox-grounding-search"
117+
checked={!!apiConfiguration.enableGrounding}
118+
onChange={(checked: boolean) =>
119+
setApiConfigurationField("enableGrounding", checked)
120+
}>
121+
{t("settings:providers.geminiParameters.groundingSearch.title")}
122+
</Checkbox>
123+
<div className="text-sm text-vscode-descriptionForeground mb-3 mt-1.5">
124+
{t("settings:providers.geminiParameters.groundingSearch.description")}
125+
</div>
126+
</>
127+
)}
128+
{apiConfiguration.apiModelId === ANTHROPIC_VERTEX_1M_CONTEXT_MODEL_ID && (
129+
<>
130+
<Checkbox
131+
data-testid="checkbox-1m-context"
132+
checked={!!apiConfiguration.vertex1MContext}
133+
onChange={(checked: boolean) =>
134+
setApiConfigurationField("vertex1MContext", checked)
135+
}>
136+
{t("settings:providers.vertex1MContextLabel")}
137+
</Checkbox>
138+
<div className="text-sm text-vscode-descriptionForeground mt-1 ml-6">
139+
{t("settings:providers.vertex1MContextDescription")}
140+
</div>
141+
</>
142+
)}
117143
</div>
118-
</div>
119-
)}
144+
)}
120145
</>
121146
)
122147
}

webview-ui/src/components/settings/providers/__tests__/Vertex.spec.tsx

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { render, screen } from "@testing-library/react"
22
import userEvent from "@testing-library/user-event"
33
import { Vertex } from "../Vertex"
44
import type { ProviderSettings } from "@roo-code/types"
5-
import { VERTEX_REGIONS } from "@roo-code/types"
5+
import { ANTHROPIC_VERTEX_1M_CONTEXT_MODEL_ID, VERTEX_REGIONS } from "@roo-code/types"
66

77
vi.mock("@vscode/webview-ui-toolkit/react", () => ({
88
VSCodeTextField: ({ children, value, onInput, type }: any) => (
@@ -282,4 +282,80 @@ describe("Vertex", () => {
282282
expect(mockSetApiConfigurationField).toHaveBeenCalledTimes(2)
283283
})
284284
})
285+
286+
describe("1M Context Checkbox", () => {
287+
it("should render 1M context checkbox unchecked by default for Claude Sonnet 4", () => {
288+
const apiConfiguration = {
289+
...defaultApiConfiguration,
290+
apiModelId: ANTHROPIC_VERTEX_1M_CONTEXT_MODEL_ID,
291+
}
292+
render(
293+
<Vertex apiConfiguration={apiConfiguration} setApiConfigurationField={mockSetApiConfigurationField} />,
294+
)
295+
296+
const oneMContextCheckbox = screen.getByTestId("checkbox-1m-context")
297+
const checkbox = oneMContextCheckbox.querySelector("input[type='checkbox']") as HTMLInputElement
298+
expect(checkbox.checked).toBe(false)
299+
})
300+
301+
it("should NOT render 1M context checkbox for other models", () => {
302+
const apiConfiguration = { ...defaultApiConfiguration, apiModelId: "gemini-2.0-flash-001" }
303+
render(
304+
<Vertex apiConfiguration={apiConfiguration} setApiConfigurationField={mockSetApiConfigurationField} />,
305+
)
306+
307+
const oneMContextCheckbox = screen.queryByTestId("checkbox-1m-context")
308+
expect(oneMContextCheckbox).toBeNull()
309+
})
310+
311+
it("should NOT render 1M context checkbox when fromWelcomeView is true", () => {
312+
const apiConfiguration = {
313+
...defaultApiConfiguration,
314+
apiModelId: ANTHROPIC_VERTEX_1M_CONTEXT_MODEL_ID,
315+
}
316+
render(
317+
<Vertex
318+
apiConfiguration={apiConfiguration}
319+
setApiConfigurationField={mockSetApiConfigurationField}
320+
fromWelcomeView={true}
321+
/>,
322+
)
323+
324+
const oneMContextCheckbox = screen.queryByTestId("checkbox-1m-context")
325+
expect(oneMContextCheckbox).toBeNull()
326+
})
327+
328+
it("should render 1M context checkbox checked when vertex1MContext is true for Claude Sonnet 4", () => {
329+
const apiConfiguration = {
330+
...defaultApiConfiguration,
331+
vertex1MContext: true,
332+
apiModelId: ANTHROPIC_VERTEX_1M_CONTEXT_MODEL_ID,
333+
}
334+
render(
335+
<Vertex apiConfiguration={apiConfiguration} setApiConfigurationField={mockSetApiConfigurationField} />,
336+
)
337+
338+
const oneMContextCheckbox = screen.getByTestId("checkbox-1m-context")
339+
const checkbox = oneMContextCheckbox.querySelector("input[type='checkbox']") as HTMLInputElement
340+
expect(checkbox.checked).toBe(true)
341+
})
342+
343+
it("should call setApiConfigurationField with correct parameters when 1M context checkbox is toggled", async () => {
344+
const user = userEvent.setup()
345+
const apiConfiguration = {
346+
...defaultApiConfiguration,
347+
apiModelId: ANTHROPIC_VERTEX_1M_CONTEXT_MODEL_ID,
348+
}
349+
render(
350+
<Vertex apiConfiguration={apiConfiguration} setApiConfigurationField={mockSetApiConfigurationField} />,
351+
)
352+
353+
const oneMContextCheckbox = screen.getByTestId("checkbox-1m-context")
354+
const checkbox = oneMContextCheckbox.querySelector("input[type='checkbox']") as HTMLInputElement
355+
356+
await user.click(checkbox)
357+
358+
expect(mockSetApiConfigurationField).toHaveBeenCalledWith("vertex1MContext", true)
359+
})
360+
})
285361
})

webview-ui/src/i18n/locales/ca/settings.json

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

webview-ui/src/i18n/locales/de/settings.json

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

webview-ui/src/i18n/locales/en/settings.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,8 @@
268268
"anthropic1MContextBetaDescription": "Extends context window to 1 million tokens for Claude Sonnet 4",
269269
"awsBedrock1MContextBetaLabel": "Enable 1M context window (Beta)",
270270
"awsBedrock1MContextBetaDescription": "Extends context window to 1 million tokens for Claude Sonnet 4",
271+
"vertex1MContextLabel": "Enable 1M context window (Preview)",
272+
"vertex1MContextDescription": "Extends context window to 1 million tokens for Claude Sonnet 4. This is a preview feature and may not be available for all users.",
271273
"cerebrasApiKey": "Cerebras API Key",
272274
"getCerebrasApiKey": "Get Cerebras API Key",
273275
"chutesApiKey": "Chutes API Key",

0 commit comments

Comments
 (0)