Skip to content

Commit d388654

Browse files
committed
feat(vertex): region-aware pricing for Vertex Claude Sonnet 4/4.5 with [1m] tiered pricing by region; add tests in useSelectedModel
1 parent 9d881fe commit d388654

File tree

2 files changed

+188
-2
lines changed

2 files changed

+188
-2
lines changed

webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,3 +514,109 @@ describe("useSelectedModel", () => {
514514
})
515515
})
516516
})
517+
518+
describe("vertex provider pricing with region and [1m] tiers", () => {
519+
beforeEach(() => {
520+
// Keep other hooks stable/inert for Vertex path
521+
;(mockUseRouterModels as any).mockReturnValue({
522+
data: { openrouter: {}, requesty: {}, glama: {}, unbound: {}, litellm: {}, "io-intelligence": {} },
523+
isLoading: false,
524+
isError: false,
525+
})
526+
;(mockUseOpenRouterModelProviders as any).mockReturnValue({
527+
data: {},
528+
isLoading: false,
529+
isError: false,
530+
})
531+
})
532+
533+
it("applies global pricing for Sonnet 4 under 200k (no [1m])", () => {
534+
const apiConfiguration: ProviderSettings = {
535+
apiProvider: "vertex",
536+
apiModelId: "claude-sonnet-4@20250514",
537+
vertexRegion: "global",
538+
}
539+
540+
const wrapper = createWrapper()
541+
const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper })
542+
543+
expect(result.current.provider).toBe("vertex")
544+
expect(result.current.id).toBe("claude-sonnet-4@20250514")
545+
expect(result.current.info?.inputPrice).toBe(3.0)
546+
expect(result.current.info?.outputPrice).toBe(15.0)
547+
expect(result.current.info?.cacheWritesPrice).toBe(3.75)
548+
expect(result.current.info?.cacheReadsPrice).toBe(0.3)
549+
})
550+
551+
it("applies global pricing for Sonnet 4 over 200k ([1m])", () => {
552+
const apiConfiguration: ProviderSettings = {
553+
apiProvider: "vertex",
554+
apiModelId: "claude-sonnet-4@20250514[1m]",
555+
vertexRegion: "global",
556+
}
557+
558+
const wrapper = createWrapper()
559+
const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper })
560+
561+
expect(result.current.provider).toBe("vertex")
562+
expect(result.current.id).toBe("claude-sonnet-4@20250514[1m]")
563+
expect(result.current.info?.inputPrice).toBe(6.0)
564+
expect(result.current.info?.outputPrice).toBe(22.5)
565+
expect(result.current.info?.cacheWritesPrice).toBe(7.5)
566+
expect(result.current.info?.cacheReadsPrice).toBe(0.6)
567+
})
568+
569+
it("applies regional pricing for Sonnet 4.5 under 200k in us-east5", () => {
570+
const apiConfiguration: ProviderSettings = {
571+
apiProvider: "vertex",
572+
apiModelId: "claude-sonnet-4-5@20250929",
573+
vertexRegion: "us-east5",
574+
}
575+
576+
const wrapper = createWrapper()
577+
const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper })
578+
579+
expect(result.current.provider).toBe("vertex")
580+
expect(result.current.id).toBe("claude-sonnet-4-5@20250929")
581+
expect(result.current.info?.inputPrice).toBe(3.3)
582+
expect(result.current.info?.outputPrice).toBe(16.5)
583+
expect(result.current.info?.cacheWritesPrice).toBe(4.13)
584+
expect(result.current.info?.cacheReadsPrice).toBe(0.33)
585+
})
586+
587+
it("applies regional pricing for Sonnet 4.5 over 200k ([1m]) in us-east5", () => {
588+
const apiConfiguration: ProviderSettings = {
589+
apiProvider: "vertex",
590+
apiModelId: "claude-sonnet-4-5@20250929[1m]",
591+
vertexRegion: "us-east5",
592+
}
593+
594+
const wrapper = createWrapper()
595+
const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper })
596+
597+
expect(result.current.provider).toBe("vertex")
598+
expect(result.current.id).toBe("claude-sonnet-4-5@20250929[1m]")
599+
expect(result.current.info?.inputPrice).toBe(6.6)
600+
expect(result.current.info?.outputPrice).toBe(24.75)
601+
expect(result.current.info?.cacheWritesPrice).toBe(8.25)
602+
expect(result.current.info?.cacheReadsPrice).toBe(0.66)
603+
})
604+
605+
it("applies global pricing for Sonnet 4.5 over 200k ([1m]) in non-regional areas (e.g., us-central1)", () => {
606+
const apiConfiguration: ProviderSettings = {
607+
apiProvider: "vertex",
608+
apiModelId: "claude-sonnet-4-5@20250929[1m]",
609+
vertexRegion: "us-central1", // treated as global pricing per rules
610+
}
611+
612+
const wrapper = createWrapper()
613+
const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper })
614+
615+
expect(result.current.provider).toBe("vertex")
616+
expect(result.current.id).toBe("claude-sonnet-4-5@20250929[1m]")
617+
expect(result.current.info?.inputPrice).toBe(6.0)
618+
expect(result.current.info?.outputPrice).toBe(22.5)
619+
expect(result.current.info?.cacheWritesPrice).toBe(7.5)
620+
expect(result.current.info?.cacheReadsPrice).toBe(0.6)
621+
})
622+
})

webview-ui/src/components/ui/hooks/useSelectedModel.ts

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -214,8 +214,88 @@ function getSelectedModel({
214214
}
215215
case "vertex": {
216216
const id = apiConfiguration.apiModelId ?? vertexDefaultModelId
217-
const info = vertexModels[id as keyof typeof vertexModels]
218-
return { id, info }
217+
const baseInfo = vertexModels[id as keyof typeof vertexModels] as ModelInfo
218+
219+
// If the model is not known, surface the selected id with undefined info
220+
if (!baseInfo) {
221+
return { id, info: undefined }
222+
}
223+
224+
// Region-aware, tiered pricing for Vertex Claude Sonnet models
225+
// Source: https://cloud.google.com/vertex-ai/generative-ai/pricing
226+
//
227+
// Rules implemented:
228+
// - Sonnet 4 pricing is the same globally in all regions
229+
// • Under 200k Input Tokens:
230+
// Input: $3, Output: $15, Cache Write: $3.75, Cache Hit: $0.30
231+
// • Over 200k Input Tokens ([1m] variants):
232+
// Input: $6, Output: $22.50, Cache Write: $7.50, Cache Hit: $0.60
233+
//
234+
// - Sonnet 4.5 has different pricing per region and per input context size
235+
// • Global region (all regions except us-east5, europe-west1, asia-southeast1)
236+
// - Under 200k Input Tokens:
237+
// Input: $3.00, Output: $15.00, Cache Write: $3.75, Cache Hit: $0.30
238+
// - Over 200k Input Tokens ([1m] variants):
239+
// Input: $6.00, Output: $22.50, Cache Write: $7.50, Cache Hit: $0.60
240+
// • Regional (us-east5, europe-west1, asia-southeast1)
241+
// - Under 200k Input Tokens:
242+
// Input: $3.30, Output: $16.50, Cache Write: $4.13, Cache Hit: $0.33
243+
// - Over 200k Input Tokens ([1m] variants):
244+
// Input: $6.60, Output: $24.75, Cache Write: $8.25, Cache Hit: $0.66
245+
//
246+
// We derive "over 200k" from Roo's explicit [1m] model variants and the selected Vertex region.
247+
const region = apiConfiguration.vertexRegion ?? "global"
248+
const is1m = id.endsWith("[1m]")
249+
const isSonnet45 = id.startsWith("claude-sonnet-4-5@20250929")
250+
const isSonnet4 = id.startsWith("claude-sonnet-4@20250514")
251+
const regionalPricingRegions = new Set(["us-east5", "europe-west1", "asia-southeast1"])
252+
const useRegionalPricing = regionalPricingRegions.has(region)
253+
254+
let adjustedInfo: ModelInfo = baseInfo as ModelInfo
255+
256+
if (isSonnet45) {
257+
if (is1m) {
258+
// Over 200k (1M beta)
259+
adjustedInfo = {
260+
...baseInfo,
261+
inputPrice: useRegionalPricing ? (6.6 as number) : (6.0 as number),
262+
outputPrice: useRegionalPricing ? (24.75 as number) : (22.5 as number),
263+
cacheWritesPrice: useRegionalPricing ? (8.25 as number) : (7.5 as number),
264+
cacheReadsPrice: useRegionalPricing ? (0.66 as number) : (0.6 as number),
265+
} as ModelInfo
266+
} else {
267+
// Under 200k
268+
adjustedInfo = {
269+
...baseInfo,
270+
inputPrice: useRegionalPricing ? (3.3 as number) : (3.0 as number),
271+
outputPrice: useRegionalPricing ? (16.5 as number) : (15.0 as number),
272+
cacheWritesPrice: useRegionalPricing ? (4.13 as number) : (3.75 as number),
273+
cacheReadsPrice: useRegionalPricing ? (0.33 as number) : (0.3 as number),
274+
} as ModelInfo
275+
}
276+
} else if (isSonnet4) {
277+
if (is1m) {
278+
// Over 200k (1M beta) - global pricing
279+
adjustedInfo = {
280+
...baseInfo,
281+
inputPrice: 6.0 as number,
282+
outputPrice: 22.5 as number,
283+
cacheWritesPrice: 7.5 as number,
284+
cacheReadsPrice: 0.6 as number,
285+
} as ModelInfo
286+
} else {
287+
// Under 200k - global pricing
288+
adjustedInfo = {
289+
...baseInfo,
290+
inputPrice: 3.0 as number,
291+
outputPrice: 15.0 as number,
292+
cacheWritesPrice: 3.75 as number,
293+
cacheReadsPrice: 0.3 as number,
294+
} as ModelInfo
295+
}
296+
}
297+
298+
return { id, info: adjustedInfo }
219299
}
220300
case "gemini": {
221301
const id = apiConfiguration.apiModelId ?? geminiDefaultModelId

0 commit comments

Comments
 (0)