Просмотр исходного кода

feat: Add Vercel AI Gateway provider integration (#7396)

Co-authored-by: daniel-lxs <[email protected]>
Co-authored-by: cte <[email protected]>
Josh 4 месяцев назад
Родитель
Сommit
934bfd0a54
46 измененных файлов с 1492 добавлено и 11 удалено
  1. 1 1
      packages/types/npm/package.metadata.json
  2. 1 0
      packages/types/src/global-settings.ts
  3. 11 0
      packages/types/src/provider-settings.ts
  4. 1 0
      packages/types/src/providers/index.ts
  5. 102 0
      packages/types/src/providers/vercel-ai-gateway.ts
  6. 9 9
      pnpm-lock.yaml
  7. 3 0
      src/api/index.ts
  8. 383 0
      src/api/providers/__tests__/vercel-ai-gateway.spec.ts
  9. 317 0
      src/api/providers/fetchers/__tests__/vercel-ai-gateway.spec.ts
  10. 4 0
      src/api/providers/fetchers/modelCache.ts
  11. 120 0
      src/api/providers/fetchers/vercel-ai-gateway.ts
  12. 1 0
      src/api/providers/index.ts
  13. 115 0
      src/api/providers/vercel-ai-gateway.ts
  14. 233 0
      src/api/transform/caching/__tests__/vercel-ai-gateway.spec.ts
  15. 30 0
      src/api/transform/caching/vercel-ai-gateway.ts
  16. 5 0
      src/core/webview/__tests__/ClineProvider.spec.ts
  17. 6 0
      src/core/webview/__tests__/webviewMessageHandler.spec.ts
  18. 1 0
      src/core/webview/webviewMessageHandler.ts
  19. 1 1
      src/package.json
  20. 2 0
      src/shared/api.ts
  21. 13 0
      webview-ui/src/components/settings/ApiOptions.tsx
  22. 1 0
      webview-ui/src/components/settings/ModelPicker.tsx
  23. 1 0
      webview-ui/src/components/settings/constants.ts
  24. 77 0
      webview-ui/src/components/settings/providers/VercelAiGateway.tsx
  25. 1 0
      webview-ui/src/components/settings/providers/index.ts
  26. 6 0
      webview-ui/src/components/ui/hooks/useSelectedModel.ts
  27. 2 0
      webview-ui/src/i18n/locales/ca/settings.json
  28. 2 0
      webview-ui/src/i18n/locales/de/settings.json
  29. 2 0
      webview-ui/src/i18n/locales/en/settings.json
  30. 2 0
      webview-ui/src/i18n/locales/es/settings.json
  31. 2 0
      webview-ui/src/i18n/locales/fr/settings.json
  32. 2 0
      webview-ui/src/i18n/locales/hi/settings.json
  33. 2 0
      webview-ui/src/i18n/locales/id/settings.json
  34. 2 0
      webview-ui/src/i18n/locales/it/settings.json
  35. 2 0
      webview-ui/src/i18n/locales/ja/settings.json
  36. 2 0
      webview-ui/src/i18n/locales/ko/settings.json
  37. 2 0
      webview-ui/src/i18n/locales/nl/settings.json
  38. 2 0
      webview-ui/src/i18n/locales/pl/settings.json
  39. 2 0
      webview-ui/src/i18n/locales/pt-BR/settings.json
  40. 2 0
      webview-ui/src/i18n/locales/ru/settings.json
  41. 2 0
      webview-ui/src/i18n/locales/tr/settings.json
  42. 2 0
      webview-ui/src/i18n/locales/vi/settings.json
  43. 2 0
      webview-ui/src/i18n/locales/zh-CN/settings.json
  44. 2 0
      webview-ui/src/i18n/locales/zh-TW/settings.json
  45. 1 0
      webview-ui/src/utils/__tests__/validate.test.ts
  46. 10 0
      webview-ui/src/utils/validate.ts

+ 1 - 1
packages/types/npm/package.metadata.json

@@ -1,6 +1,6 @@
 {
 	"name": "@roo-code/types",
-	"version": "1.60.0",
+	"version": "1.61.0",
 	"description": "TypeScript type definitions for Roo Code.",
 	"publishConfig": {
 		"access": "public",

+ 1 - 0
packages/types/src/global-settings.ts

@@ -198,6 +198,7 @@ export const SECRET_STATE_KEYS = [
 	"fireworksApiKey",
 	"featherlessApiKey",
 	"ioIntelligenceApiKey",
+	"vercelAiGatewayApiKey",
 ] as const satisfies readonly (keyof ProviderSettings)[]
 export type SecretState = Pick<ProviderSettings, (typeof SECRET_STATE_KEYS)[number]>
 

+ 11 - 0
packages/types/src/provider-settings.ts

@@ -66,6 +66,7 @@ export const providerNames = [
 	"featherless",
 	"io-intelligence",
 	"roo",
+	"vercel-ai-gateway",
 ] as const
 
 export const providerNamesSchema = z.enum(providerNames)
@@ -321,6 +322,11 @@ const rooSchema = apiModelIdProviderModelSchema.extend({
 	// No additional fields needed - uses cloud authentication
 })
 
+const vercelAiGatewaySchema = baseProviderSettingsSchema.extend({
+	vercelAiGatewayApiKey: z.string().optional(),
+	vercelAiGatewayModelId: z.string().optional(),
+})
+
 const defaultSchema = z.object({
 	apiProvider: z.undefined(),
 })
@@ -360,6 +366,7 @@ export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProv
 	ioIntelligenceSchema.merge(z.object({ apiProvider: z.literal("io-intelligence") })),
 	qwenCodeSchema.merge(z.object({ apiProvider: z.literal("qwen-code") })),
 	rooSchema.merge(z.object({ apiProvider: z.literal("roo") })),
+	vercelAiGatewaySchema.merge(z.object({ apiProvider: z.literal("vercel-ai-gateway") })),
 	defaultSchema,
 ])
 
@@ -399,6 +406,7 @@ export const providerSettingsSchema = z.object({
 	...ioIntelligenceSchema.shape,
 	...qwenCodeSchema.shape,
 	...rooSchema.shape,
+	...vercelAiGatewaySchema.shape,
 	...codebaseIndexProviderSchema.shape,
 })
 
@@ -425,6 +433,7 @@ export const MODEL_ID_KEYS: Partial<keyof ProviderSettings>[] = [
 	"litellmModelId",
 	"huggingFaceModelId",
 	"ioIntelligenceModelId",
+	"vercelAiGatewayModelId",
 ]
 
 export const getModelId = (settings: ProviderSettings): string | undefined => {
@@ -541,6 +550,7 @@ export const MODELS_BY_PROVIDER: Record<
 	openrouter: { id: "openrouter", label: "OpenRouter", models: [] },
 	requesty: { id: "requesty", label: "Requesty", models: [] },
 	unbound: { id: "unbound", label: "Unbound", models: [] },
+	"vercel-ai-gateway": { id: "vercel-ai-gateway", label: "Vercel AI Gateway", models: [] },
 }
 
 export const dynamicProviders = [
@@ -550,6 +560,7 @@ export const dynamicProviders = [
 	"openrouter",
 	"requesty",
 	"unbound",
+	"vercel-ai-gateway",
 ] as const satisfies readonly ProviderName[]
 
 export type DynamicProvider = (typeof dynamicProviders)[number]

+ 1 - 0
packages/types/src/providers/index.ts

@@ -27,4 +27,5 @@ export * from "./unbound.js"
 export * from "./vertex.js"
 export * from "./vscode-llm.js"
 export * from "./xai.js"
+export * from "./vercel-ai-gateway.js"
 export * from "./zai.js"

+ 102 - 0
packages/types/src/providers/vercel-ai-gateway.ts

@@ -0,0 +1,102 @@
+import type { ModelInfo } from "../model.js"
+
+// https://ai-gateway.vercel.sh/v1/
+export const vercelAiGatewayDefaultModelId = "anthropic/claude-sonnet-4"
+
+export const VERCEL_AI_GATEWAY_PROMPT_CACHING_MODELS = new Set([
+	"anthropic/claude-3-haiku",
+	"anthropic/claude-3-opus",
+	"anthropic/claude-3.5-haiku",
+	"anthropic/claude-3.5-sonnet",
+	"anthropic/claude-3.7-sonnet",
+	"anthropic/claude-opus-4",
+	"anthropic/claude-opus-4.1",
+	"anthropic/claude-sonnet-4",
+	"openai/gpt-4.1",
+	"openai/gpt-4.1-mini",
+	"openai/gpt-4.1-nano",
+	"openai/gpt-4o",
+	"openai/gpt-4o-mini",
+	"openai/gpt-5",
+	"openai/gpt-5-mini",
+	"openai/gpt-5-nano",
+	"openai/o1",
+	"openai/o3",
+	"openai/o3-mini",
+	"openai/o4-mini",
+])
+
+export const VERCEL_AI_GATEWAY_VISION_ONLY_MODELS = new Set([
+	"alibaba/qwen-3-14b",
+	"alibaba/qwen-3-235b",
+	"alibaba/qwen-3-30b",
+	"alibaba/qwen-3-32b",
+	"alibaba/qwen3-coder",
+	"amazon/nova-pro",
+	"anthropic/claude-3.5-haiku",
+	"google/gemini-1.5-flash-8b",
+	"google/gemini-2.0-flash-thinking",
+	"google/gemma-3-27b",
+	"mistral/devstral-small",
+	"xai/grok-vision-beta",
+])
+
+export const VERCEL_AI_GATEWAY_VISION_AND_TOOLS_MODELS = new Set([
+	"amazon/nova-lite",
+	"anthropic/claude-3-haiku",
+	"anthropic/claude-3-opus",
+	"anthropic/claude-3-sonnet",
+	"anthropic/claude-3.5-sonnet",
+	"anthropic/claude-3.7-sonnet",
+	"anthropic/claude-opus-4",
+	"anthropic/claude-opus-4.1",
+	"anthropic/claude-sonnet-4",
+	"google/gemini-1.5-flash",
+	"google/gemini-1.5-pro",
+	"google/gemini-2.0-flash",
+	"google/gemini-2.0-flash-lite",
+	"google/gemini-2.0-pro",
+	"google/gemini-2.5-flash",
+	"google/gemini-2.5-flash-lite",
+	"google/gemini-2.5-pro",
+	"google/gemini-exp",
+	"meta/llama-3.2-11b",
+	"meta/llama-3.2-90b",
+	"meta/llama-3.3",
+	"meta/llama-4-maverick",
+	"meta/llama-4-scout",
+	"mistral/pixtral-12b",
+	"mistral/pixtral-large",
+	"moonshotai/kimi-k2",
+	"openai/gpt-4-turbo",
+	"openai/gpt-4.1",
+	"openai/gpt-4.1-mini",
+	"openai/gpt-4.1-nano",
+	"openai/gpt-4.5-preview",
+	"openai/gpt-4o",
+	"openai/gpt-4o-mini",
+	"openai/gpt-oss-120b",
+	"openai/gpt-oss-20b",
+	"openai/o3",
+	"openai/o3-pro",
+	"openai/o4-mini",
+	"vercel/v0-1.0-md",
+	"xai/grok-2-vision",
+	"zai/glm-4.5v",
+])
+
+export const vercelAiGatewayDefaultModelInfo: ModelInfo = {
+	maxTokens: 64000,
+	contextWindow: 200000,
+	supportsImages: true,
+	supportsComputerUse: true,
+	supportsPromptCache: true,
+	inputPrice: 3,
+	outputPrice: 15,
+	cacheWritesPrice: 3.75,
+	cacheReadsPrice: 0.3,
+	description:
+		"Claude Sonnet 4 significantly improves on Sonnet 3.7's industry-leading capabilities, excelling in coding with a state-of-the-art 72.7% on SWE-bench. The model balances performance and efficiency for internal and external use cases, with enhanced steerability for greater control over implementations. While not matching Opus 4 in most domains, it delivers an optimal mix of capability and practicality.",
+}
+
+export const VERCEL_AI_GATEWAY_DEFAULT_TEMPERATURE = 0.7

+ 9 - 9
pnpm-lock.yaml

@@ -584,8 +584,8 @@ importers:
         specifier: ^1.14.0
         version: 1.14.0([email protected])
       '@roo-code/cloud':
-        specifier: ^0.21.0
-        version: 0.21.0
+        specifier: ^0.22.0
+        version: 0.22.0
       '@roo-code/ipc':
         specifier: workspace:^
         version: link:../packages/ipc
@@ -3262,11 +3262,11 @@ packages:
     cpu: [x64]
     os: [win32]
 
-  '@roo-code/[email protected]1.0':
-    resolution: {integrity: sha512-yNVybIjaS7Hy8GwDtGJc76N1WpCXGaCSlAEsW7VGjnojpxaIzV2GcJP1j1hg5q8HqLQnU4ixV0qXxOkxwhkEiA==}
+  '@roo-code/[email protected]2.0':
+    resolution: {integrity: sha512-s1d4wcDYeDzcwr+YypMWDlNKL4f2osOZ3NoIlD36LCfFeMs+hnluZPS1oXX3WHtmPDC76vSzPMfwW2Ef41hEoA==}
 
-  '@roo-code/[email protected]0.0':
-    resolution: {integrity: sha512-tQO6njPr/ZDNBoSHQg1/dpxfVEYeUzpKcernUxgJzmttn1zJbS0sc3CfUyPYOfYKB331z6O3KFUpaiqYFje1wA==}
+  '@roo-code/[email protected]1.0':
+    resolution: {integrity: sha512-YJdFc6aYfaZ8EN08KbWaKLehRr1dcN3G3CzDjpppb08iehSEUZMycax/ryP5/G4vl34HTdtzyHNMboDen5ElUg==}
 
   '@sec-ant/[email protected]':
     resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==}
@@ -12563,9 +12563,9 @@ snapshots:
   '@rollup/[email protected]':
     optional: true
 
-  '@roo-code/[email protected]1.0':
+  '@roo-code/[email protected]2.0':
     dependencies:
-      '@roo-code/types': 1.60.0
+      '@roo-code/types': 1.61.0
       ioredis: 5.6.1
       p-wait-for: 5.0.2
       socket.io-client: 4.8.1
@@ -12575,7 +12575,7 @@ snapshots:
       - supports-color
       - utf-8-validate
 
-  '@roo-code/[email protected]0.0':
+  '@roo-code/[email protected]1.0':
     dependencies:
       zod: 3.25.76
 

+ 3 - 0
src/api/index.ts

@@ -38,6 +38,7 @@ import {
 	FireworksHandler,
 	RooHandler,
 	FeatherlessHandler,
+	VercelAiGatewayHandler,
 } from "./providers"
 import { NativeOllamaHandler } from "./providers/native-ollama"
 
@@ -151,6 +152,8 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler {
 			return new RooHandler(options)
 		case "featherless":
 			return new FeatherlessHandler(options)
+		case "vercel-ai-gateway":
+			return new VercelAiGatewayHandler(options)
 		default:
 			apiProvider satisfies "gemini-cli" | undefined
 			return new AnthropicHandler(options)

+ 383 - 0
src/api/providers/__tests__/vercel-ai-gateway.spec.ts

@@ -0,0 +1,383 @@
+// npx vitest run src/api/providers/__tests__/vercel-ai-gateway.spec.ts
+
+// Mock vscode first to avoid import errors
+vitest.mock("vscode", () => ({}))
+
+import { Anthropic } from "@anthropic-ai/sdk"
+import OpenAI from "openai"
+
+import { VercelAiGatewayHandler } from "../vercel-ai-gateway"
+import { ApiHandlerOptions } from "../../../shared/api"
+import { vercelAiGatewayDefaultModelId, VERCEL_AI_GATEWAY_DEFAULT_TEMPERATURE } from "@roo-code/types"
+
+// Mock dependencies
+vitest.mock("openai")
+vitest.mock("delay", () => ({ default: vitest.fn(() => Promise.resolve()) }))
+vitest.mock("../fetchers/modelCache", () => ({
+	getModels: vitest.fn().mockImplementation(() => {
+		return Promise.resolve({
+			"anthropic/claude-sonnet-4": {
+				maxTokens: 64000,
+				contextWindow: 200000,
+				supportsImages: true,
+				supportsPromptCache: true,
+				inputPrice: 3,
+				outputPrice: 15,
+				cacheWritesPrice: 3.75,
+				cacheReadsPrice: 0.3,
+				description: "Claude Sonnet 4",
+				supportsComputerUse: true,
+			},
+			"anthropic/claude-3.5-haiku": {
+				maxTokens: 32000,
+				contextWindow: 200000,
+				supportsImages: true,
+				supportsPromptCache: true,
+				inputPrice: 1,
+				outputPrice: 5,
+				cacheWritesPrice: 1.25,
+				cacheReadsPrice: 0.1,
+				description: "Claude 3.5 Haiku",
+				supportsComputerUse: false,
+			},
+			"openai/gpt-4o": {
+				maxTokens: 16000,
+				contextWindow: 128000,
+				supportsImages: true,
+				supportsPromptCache: true,
+				inputPrice: 2.5,
+				outputPrice: 10,
+				cacheWritesPrice: 3.125,
+				cacheReadsPrice: 0.25,
+				description: "GPT-4o",
+				supportsComputerUse: true,
+			},
+		})
+	}),
+}))
+
+vitest.mock("../../transform/caching/vercel-ai-gateway", () => ({
+	addCacheBreakpoints: vitest.fn(),
+}))
+
+const mockCreate = vitest.fn()
+const mockConstructor = vitest.fn()
+
+;(OpenAI as any).mockImplementation(() => ({
+	chat: {
+		completions: {
+			create: mockCreate,
+		},
+	},
+}))
+;(OpenAI as any).mockImplementation = mockConstructor.mockReturnValue({
+	chat: {
+		completions: {
+			create: mockCreate,
+		},
+	},
+})
+
+describe("VercelAiGatewayHandler", () => {
+	const mockOptions: ApiHandlerOptions = {
+		vercelAiGatewayApiKey: "test-key",
+		vercelAiGatewayModelId: "anthropic/claude-sonnet-4",
+	}
+
+	beforeEach(() => {
+		vitest.clearAllMocks()
+		mockCreate.mockClear()
+		mockConstructor.mockClear()
+	})
+
+	it("initializes with correct options", () => {
+		const handler = new VercelAiGatewayHandler(mockOptions)
+		expect(handler).toBeInstanceOf(VercelAiGatewayHandler)
+
+		expect(OpenAI).toHaveBeenCalledWith({
+			baseURL: "https://ai-gateway.vercel.sh/v1",
+			apiKey: mockOptions.vercelAiGatewayApiKey,
+			defaultHeaders: expect.objectContaining({
+				"HTTP-Referer": "https://github.com/RooVetGit/Roo-Cline",
+				"X-Title": "Roo Code",
+				"User-Agent": expect.stringContaining("RooCode/"),
+			}),
+		})
+	})
+
+	describe("fetchModel", () => {
+		it("returns correct model info when options are provided", async () => {
+			const handler = new VercelAiGatewayHandler(mockOptions)
+			const result = await handler.fetchModel()
+
+			expect(result.id).toBe(mockOptions.vercelAiGatewayModelId)
+			expect(result.info.maxTokens).toBe(64000)
+			expect(result.info.contextWindow).toBe(200000)
+			expect(result.info.supportsImages).toBe(true)
+			expect(result.info.supportsPromptCache).toBe(true)
+			expect(result.info.supportsComputerUse).toBe(true)
+		})
+
+		it("returns default model info when options are not provided", async () => {
+			const handler = new VercelAiGatewayHandler({})
+			const result = await handler.fetchModel()
+			expect(result.id).toBe(vercelAiGatewayDefaultModelId)
+			expect(result.info.supportsPromptCache).toBe(true)
+		})
+
+		it("uses vercel ai gateway default model when no model specified", async () => {
+			const handler = new VercelAiGatewayHandler({ vercelAiGatewayApiKey: "test-key" })
+			const result = await handler.fetchModel()
+			expect(result.id).toBe("anthropic/claude-sonnet-4")
+		})
+	})
+
+	describe("createMessage", () => {
+		beforeEach(() => {
+			mockCreate.mockImplementation(async () => ({
+				[Symbol.asyncIterator]: async function* () {
+					yield {
+						choices: [
+							{
+								delta: { content: "Test response" },
+								index: 0,
+							},
+						],
+						usage: null,
+					}
+					yield {
+						choices: [
+							{
+								delta: {},
+								index: 0,
+							},
+						],
+						usage: {
+							prompt_tokens: 10,
+							completion_tokens: 5,
+							total_tokens: 15,
+							cache_creation_input_tokens: 2,
+							prompt_tokens_details: {
+								cached_tokens: 3,
+							},
+							cost: 0.005,
+						},
+					}
+				},
+			}))
+		})
+
+		it("streams text content correctly", async () => {
+			const handler = new VercelAiGatewayHandler(mockOptions)
+			const systemPrompt = "You are a helpful assistant."
+			const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello" }]
+
+			const stream = handler.createMessage(systemPrompt, messages)
+			const chunks = []
+			for await (const chunk of stream) {
+				chunks.push(chunk)
+			}
+
+			expect(chunks).toHaveLength(2)
+			expect(chunks[0]).toEqual({
+				type: "text",
+				text: "Test response",
+			})
+			expect(chunks[1]).toEqual({
+				type: "usage",
+				inputTokens: 10,
+				outputTokens: 5,
+				cacheWriteTokens: 2,
+				cacheReadTokens: 3,
+				totalCost: 0.005,
+			})
+		})
+
+		it("uses correct temperature from options", async () => {
+			const customTemp = 0.5
+			const handler = new VercelAiGatewayHandler({
+				...mockOptions,
+				modelTemperature: customTemp,
+			})
+
+			const systemPrompt = "You are a helpful assistant."
+			const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello" }]
+
+			await handler.createMessage(systemPrompt, messages).next()
+
+			expect(mockCreate).toHaveBeenCalledWith(
+				expect.objectContaining({
+					temperature: customTemp,
+				}),
+			)
+		})
+
+		it("uses default temperature when none provided", async () => {
+			const handler = new VercelAiGatewayHandler(mockOptions)
+
+			const systemPrompt = "You are a helpful assistant."
+			const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello" }]
+
+			await handler.createMessage(systemPrompt, messages).next()
+
+			expect(mockCreate).toHaveBeenCalledWith(
+				expect.objectContaining({
+					temperature: VERCEL_AI_GATEWAY_DEFAULT_TEMPERATURE,
+				}),
+			)
+		})
+
+		it("adds cache breakpoints for supported models", async () => {
+			const { addCacheBreakpoints } = await import("../../transform/caching/vercel-ai-gateway")
+			const handler = new VercelAiGatewayHandler({
+				...mockOptions,
+				vercelAiGatewayModelId: "anthropic/claude-3.5-haiku",
+			})
+
+			const systemPrompt = "You are a helpful assistant."
+			const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello" }]
+
+			await handler.createMessage(systemPrompt, messages).next()
+
+			expect(addCacheBreakpoints).toHaveBeenCalled()
+		})
+
+		it("sets correct max_completion_tokens", async () => {
+			const handler = new VercelAiGatewayHandler(mockOptions)
+
+			const systemPrompt = "You are a helpful assistant."
+			const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello" }]
+
+			await handler.createMessage(systemPrompt, messages).next()
+
+			expect(mockCreate).toHaveBeenCalledWith(
+				expect.objectContaining({
+					max_completion_tokens: 64000, // max tokens for sonnet 4
+				}),
+			)
+		})
+
+		it("handles usage info correctly with all Vercel AI Gateway specific fields", async () => {
+			const handler = new VercelAiGatewayHandler(mockOptions)
+			const systemPrompt = "You are a helpful assistant."
+			const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello" }]
+
+			const stream = handler.createMessage(systemPrompt, messages)
+			const chunks = []
+			for await (const chunk of stream) {
+				chunks.push(chunk)
+			}
+
+			const usageChunk = chunks.find((chunk) => chunk.type === "usage")
+			expect(usageChunk).toEqual({
+				type: "usage",
+				inputTokens: 10,
+				outputTokens: 5,
+				cacheWriteTokens: 2,
+				cacheReadTokens: 3,
+				totalCost: 0.005,
+			})
+		})
+	})
+
+	describe("completePrompt", () => {
+		beforeEach(() => {
+			mockCreate.mockImplementation(async () => ({
+				choices: [
+					{
+						message: { role: "assistant", content: "Test completion response" },
+						finish_reason: "stop",
+						index: 0,
+					},
+				],
+				usage: {
+					prompt_tokens: 8,
+					completion_tokens: 4,
+					total_tokens: 12,
+				},
+			}))
+		})
+
+		it("completes prompt correctly", async () => {
+			const handler = new VercelAiGatewayHandler(mockOptions)
+			const prompt = "Complete this: Hello"
+
+			const result = await handler.completePrompt(prompt)
+
+			expect(result).toBe("Test completion response")
+			expect(mockCreate).toHaveBeenCalledWith(
+				expect.objectContaining({
+					model: "anthropic/claude-sonnet-4",
+					messages: [{ role: "user", content: prompt }],
+					stream: false,
+					temperature: VERCEL_AI_GATEWAY_DEFAULT_TEMPERATURE,
+					max_completion_tokens: 64000,
+				}),
+			)
+		})
+
+		it("uses custom temperature for completion", async () => {
+			const customTemp = 0.8
+			const handler = new VercelAiGatewayHandler({
+				...mockOptions,
+				modelTemperature: customTemp,
+			})
+
+			await handler.completePrompt("Test prompt")
+
+			expect(mockCreate).toHaveBeenCalledWith(
+				expect.objectContaining({
+					temperature: customTemp,
+				}),
+			)
+		})
+
+		it("handles completion errors correctly", async () => {
+			const handler = new VercelAiGatewayHandler(mockOptions)
+			const errorMessage = "API error"
+
+			mockCreate.mockImplementation(() => {
+				throw new Error(errorMessage)
+			})
+
+			await expect(handler.completePrompt("Test")).rejects.toThrow(
+				`Vercel AI Gateway completion error: ${errorMessage}`,
+			)
+		})
+
+		it("returns empty string when no content in response", async () => {
+			const handler = new VercelAiGatewayHandler(mockOptions)
+
+			mockCreate.mockImplementation(async () => ({
+				choices: [
+					{
+						message: { role: "assistant", content: null },
+						finish_reason: "stop",
+						index: 0,
+					},
+				],
+			}))
+
+			const result = await handler.completePrompt("Test")
+			expect(result).toBe("")
+		})
+	})
+
+	describe("temperature support", () => {
+		it("applies temperature for supported models", async () => {
+			const handler = new VercelAiGatewayHandler({
+				...mockOptions,
+				vercelAiGatewayModelId: "anthropic/claude-sonnet-4",
+				modelTemperature: 0.9,
+			})
+
+			await handler.completePrompt("Test")
+
+			expect(mockCreate).toHaveBeenCalledWith(
+				expect.objectContaining({
+					temperature: 0.9,
+				}),
+			)
+		})
+	})
+})

+ 317 - 0
src/api/providers/fetchers/__tests__/vercel-ai-gateway.spec.ts

@@ -0,0 +1,317 @@
+// npx vitest run src/api/providers/fetchers/__tests__/vercel-ai-gateway.spec.ts
+
+import axios from "axios"
+import { VERCEL_AI_GATEWAY_VISION_ONLY_MODELS, VERCEL_AI_GATEWAY_VISION_AND_TOOLS_MODELS } from "@roo-code/types"
+
+import { getVercelAiGatewayModels, parseVercelAiGatewayModel } from "../vercel-ai-gateway"
+
+vitest.mock("axios")
+const mockedAxios = axios as any
+
+describe("Vercel AI Gateway Fetchers", () => {
+	beforeEach(() => {
+		vitest.clearAllMocks()
+	})
+
+	describe("getVercelAiGatewayModels", () => {
+		const mockResponse = {
+			data: {
+				object: "list",
+				data: [
+					{
+						id: "anthropic/claude-sonnet-4",
+						object: "model",
+						created: 1640995200,
+						owned_by: "anthropic",
+						name: "Claude Sonnet 4",
+						description:
+							"Claude Sonnet 4 significantly improves on Sonnet 3.7's industry-leading capabilities",
+						context_window: 200000,
+						max_tokens: 64000,
+						type: "language",
+						pricing: {
+							input: "3.00",
+							output: "15.00",
+							input_cache_write: "3.75",
+							input_cache_read: "0.30",
+						},
+					},
+					{
+						id: "anthropic/claude-3.5-haiku",
+						object: "model",
+						created: 1640995200,
+						owned_by: "anthropic",
+						name: "Claude 3.5 Haiku",
+						description: "Claude 3.5 Haiku is fast and lightweight",
+						context_window: 200000,
+						max_tokens: 32000,
+						type: "language",
+						pricing: {
+							input: "1.00",
+							output: "5.00",
+							input_cache_write: "1.25",
+							input_cache_read: "0.10",
+						},
+					},
+					{
+						id: "dall-e-3",
+						object: "model",
+						created: 1640995200,
+						owned_by: "openai",
+						name: "DALL-E 3",
+						description: "DALL-E 3 image generation model",
+						context_window: 4000,
+						max_tokens: 1000,
+						type: "image",
+						pricing: {
+							input: "40.00",
+							output: "0.00",
+						},
+					},
+				],
+			},
+		}
+
+		it("fetches and parses models correctly", async () => {
+			mockedAxios.get.mockResolvedValueOnce(mockResponse)
+
+			const models = await getVercelAiGatewayModels()
+
+			expect(mockedAxios.get).toHaveBeenCalledWith("https://ai-gateway.vercel.sh/v1/models")
+			expect(Object.keys(models)).toHaveLength(2) // Only language models
+			expect(models["anthropic/claude-sonnet-4"]).toBeDefined()
+			expect(models["anthropic/claude-3.5-haiku"]).toBeDefined()
+		})
+
+		it("handles API errors gracefully", async () => {
+			const consoleErrorSpy = vitest.spyOn(console, "error").mockImplementation(() => {})
+			mockedAxios.get.mockRejectedValueOnce(new Error("Network error"))
+
+			const models = await getVercelAiGatewayModels()
+
+			expect(models).toEqual({})
+			expect(consoleErrorSpy).toHaveBeenCalledWith(
+				expect.stringContaining("Error fetching Vercel AI Gateway models"),
+			)
+			consoleErrorSpy.mockRestore()
+		})
+
+		it("handles invalid response schema gracefully", async () => {
+			const consoleErrorSpy = vitest.spyOn(console, "error").mockImplementation(() => {})
+			mockedAxios.get.mockResolvedValueOnce({
+				data: {
+					invalid: "response",
+					data: "not an array",
+				},
+			})
+
+			const models = await getVercelAiGatewayModels()
+
+			expect(models).toEqual({})
+			expect(consoleErrorSpy).toHaveBeenCalledWith(
+				"Vercel AI Gateway models response is invalid",
+				expect.any(Object),
+			)
+			consoleErrorSpy.mockRestore()
+		})
+
+		it("continues processing with partially valid schema", async () => {
+			const consoleErrorSpy = vitest.spyOn(console, "error").mockImplementation(() => {})
+			const invalidResponse = {
+				data: {
+					invalid_root: "response",
+					data: [
+						{
+							id: "anthropic/claude-sonnet-4",
+							object: "model",
+							created: 1640995200,
+							owned_by: "anthropic",
+							name: "Claude Sonnet 4",
+							description: "Claude Sonnet 4",
+							context_window: 200000,
+							max_tokens: 64000,
+							type: "language",
+							pricing: {
+								input: "3.00",
+								output: "15.00",
+							},
+						},
+					],
+				},
+			}
+			mockedAxios.get.mockResolvedValueOnce(invalidResponse)
+
+			const models = await getVercelAiGatewayModels()
+
+			expect(consoleErrorSpy).toHaveBeenCalled()
+			expect(models["anthropic/claude-sonnet-4"]).toBeDefined()
+			consoleErrorSpy.mockRestore()
+		})
+	})
+
+	describe("parseVercelAiGatewayModel", () => {
+		const baseModel = {
+			id: "test/model",
+			object: "model",
+			created: 1640995200,
+			owned_by: "test",
+			name: "Test Model",
+			description: "A test model",
+			context_window: 100000,
+			max_tokens: 8000,
+			type: "language",
+			pricing: {
+				input: "2.50",
+				output: "10.00",
+			},
+		}
+
+		it("parses basic model info correctly", () => {
+			const result = parseVercelAiGatewayModel({
+				id: "test/model",
+				model: baseModel,
+			})
+
+			expect(result).toEqual({
+				maxTokens: 8000,
+				contextWindow: 100000,
+				supportsImages: false,
+				supportsComputerUse: false,
+				supportsPromptCache: false,
+				inputPrice: 2500000,
+				outputPrice: 10000000,
+				cacheWritesPrice: undefined,
+				cacheReadsPrice: undefined,
+				description: "A test model",
+			})
+		})
+
+		it("parses cache pricing when available", () => {
+			const modelWithCache = {
+				...baseModel,
+				pricing: {
+					input: "3.00",
+					output: "15.00",
+					input_cache_write: "3.75",
+					input_cache_read: "0.30",
+				},
+			}
+
+			const result = parseVercelAiGatewayModel({
+				id: "anthropic/claude-sonnet-4",
+				model: modelWithCache,
+			})
+
+			expect(result).toMatchObject({
+				supportsPromptCache: true,
+				cacheWritesPrice: 3750000,
+				cacheReadsPrice: 300000,
+			})
+		})
+
+		it("detects vision-only models", () => {
+			// claude 3.5 haiku in VERCEL_AI_GATEWAY_VISION_ONLY_MODELS
+			const visionModel = {
+				...baseModel,
+				id: "anthropic/claude-3.5-haiku",
+			}
+
+			const result = parseVercelAiGatewayModel({
+				id: "anthropic/claude-3.5-haiku",
+				model: visionModel,
+			})
+
+			expect(result.supportsImages).toBe(VERCEL_AI_GATEWAY_VISION_ONLY_MODELS.has("anthropic/claude-3.5-haiku"))
+			expect(result.supportsComputerUse).toBe(false)
+		})
+
+		it("detects vision and tools models", () => {
+			// 4 sonnet in VERCEL_AI_GATEWAY_VISION_AND_TOOLS_MODELS
+			const visionToolsModel = {
+				...baseModel,
+				id: "anthropic/claude-sonnet-4",
+			}
+
+			const result = parseVercelAiGatewayModel({
+				id: "anthropic/claude-sonnet-4",
+				model: visionToolsModel,
+			})
+
+			expect(result.supportsImages).toBe(
+				VERCEL_AI_GATEWAY_VISION_AND_TOOLS_MODELS.has("anthropic/claude-sonnet-4"),
+			)
+			expect(result.supportsComputerUse).toBe(
+				VERCEL_AI_GATEWAY_VISION_AND_TOOLS_MODELS.has("anthropic/claude-sonnet-4"),
+			)
+		})
+
+		it("handles missing cache pricing", () => {
+			const modelNoCachePricing = {
+				...baseModel,
+				pricing: {
+					input: "2.50",
+					output: "10.00",
+					// No cache pricing
+				},
+			}
+
+			const result = parseVercelAiGatewayModel({
+				id: "test/model",
+				model: modelNoCachePricing,
+			})
+
+			expect(result.supportsPromptCache).toBe(false)
+			expect(result.cacheWritesPrice).toBeUndefined()
+			expect(result.cacheReadsPrice).toBeUndefined()
+		})
+
+		it("handles partial cache pricing", () => {
+			const modelPartialCachePricing = {
+				...baseModel,
+				pricing: {
+					input: "2.50",
+					output: "10.00",
+					input_cache_write: "3.00",
+					// Missing input_cache_read
+				},
+			}
+
+			const result = parseVercelAiGatewayModel({
+				id: "test/model",
+				model: modelPartialCachePricing,
+			})
+
+			expect(result.supportsPromptCache).toBe(false)
+			expect(result.cacheWritesPrice).toBe(3000000)
+			expect(result.cacheReadsPrice).toBeUndefined()
+		})
+
+		it("validates all vision model categories", () => {
+			// Test a few models from each category
+			const visionOnlyModels = ["anthropic/claude-3.5-haiku", "google/gemini-1.5-flash-8b"]
+			const visionAndToolsModels = ["anthropic/claude-sonnet-4", "openai/gpt-4o"]
+
+			visionOnlyModels.forEach((modelId) => {
+				if (VERCEL_AI_GATEWAY_VISION_ONLY_MODELS.has(modelId)) {
+					const result = parseVercelAiGatewayModel({
+						id: modelId,
+						model: { ...baseModel, id: modelId },
+					})
+					expect(result.supportsImages).toBe(true)
+					expect(result.supportsComputerUse).toBe(false)
+				}
+			})
+
+			visionAndToolsModels.forEach((modelId) => {
+				if (VERCEL_AI_GATEWAY_VISION_AND_TOOLS_MODELS.has(modelId)) {
+					const result = parseVercelAiGatewayModel({
+						id: modelId,
+						model: { ...baseModel, id: modelId },
+					})
+					expect(result.supportsImages).toBe(true)
+					expect(result.supportsComputerUse).toBe(true)
+				}
+			})
+		})
+	})
+})

+ 4 - 0
src/api/providers/fetchers/modelCache.ts

@@ -10,6 +10,7 @@ import { RouterName, ModelRecord } from "../../../shared/api"
 import { fileExistsAtPath } from "../../../utils/fs"
 
 import { getOpenRouterModels } from "./openrouter"
+import { getVercelAiGatewayModels } from "./vercel-ai-gateway"
 import { getRequestyModels } from "./requesty"
 import { getGlamaModels } from "./glama"
 import { getUnboundModels } from "./unbound"
@@ -81,6 +82,9 @@ export const getModels = async (options: GetModelsOptions): Promise<ModelRecord>
 			case "io-intelligence":
 				models = await getIOIntelligenceModels(options.apiKey)
 				break
+			case "vercel-ai-gateway":
+				models = await getVercelAiGatewayModels()
+				break
 			default: {
 				// Ensures router is exhaustively checked if RouterName is a strict union
 				const exhaustiveCheck: never = provider

+ 120 - 0
src/api/providers/fetchers/vercel-ai-gateway.ts

@@ -0,0 +1,120 @@
+import axios from "axios"
+import { z } from "zod"
+
+import type { ModelInfo } from "@roo-code/types"
+import { VERCEL_AI_GATEWAY_VISION_ONLY_MODELS, VERCEL_AI_GATEWAY_VISION_AND_TOOLS_MODELS } from "@roo-code/types"
+
+import type { ApiHandlerOptions } from "../../../shared/api"
+import { parseApiPrice } from "../../../shared/cost"
+
+/**
+ * VercelAiGatewayPricing
+ */
+
+const vercelAiGatewayPricingSchema = z.object({
+	input: z.string(),
+	output: z.string(),
+	input_cache_write: z.string().optional(),
+	input_cache_read: z.string().optional(),
+})
+
+/**
+ * VercelAiGatewayModel
+ */
+
+const vercelAiGatewayModelSchema = z.object({
+	id: z.string(),
+	object: z.string(),
+	created: z.number(),
+	owned_by: z.string(),
+	name: z.string(),
+	description: z.string(),
+	context_window: z.number(),
+	max_tokens: z.number(),
+	type: z.string(),
+	pricing: vercelAiGatewayPricingSchema,
+})
+
+export type VercelAiGatewayModel = z.infer<typeof vercelAiGatewayModelSchema>
+
+/**
+ * VercelAiGatewayModelsResponse
+ */
+
+const vercelAiGatewayModelsResponseSchema = z.object({
+	object: z.string(),
+	data: z.array(vercelAiGatewayModelSchema),
+})
+
+type VercelAiGatewayModelsResponse = z.infer<typeof vercelAiGatewayModelsResponseSchema>
+
+/**
+ * getVercelAiGatewayModels
+ */
+
+export async function getVercelAiGatewayModels(options?: ApiHandlerOptions): Promise<Record<string, ModelInfo>> {
+	const models: Record<string, ModelInfo> = {}
+	const baseURL = "https://ai-gateway.vercel.sh/v1"
+
+	try {
+		const response = await axios.get<VercelAiGatewayModelsResponse>(`${baseURL}/models`)
+		const result = vercelAiGatewayModelsResponseSchema.safeParse(response.data)
+		const data = result.success ? result.data.data : response.data.data
+
+		if (!result.success) {
+			console.error("Vercel AI Gateway models response is invalid", result.error.format())
+		}
+
+		for (const model of data) {
+			const { id } = model
+
+			// Only include language models
+			if (model.type !== "language") {
+				continue
+			}
+
+			models[id] = parseVercelAiGatewayModel({
+				id,
+				model,
+			})
+		}
+	} catch (error) {
+		console.error(
+			`Error fetching Vercel AI Gateway models: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
+		)
+	}
+
+	return models
+}
+
+/**
+ * parseVercelAiGatewayModel
+ */
+
+export const parseVercelAiGatewayModel = ({ id, model }: { id: string; model: VercelAiGatewayModel }): ModelInfo => {
+	const cacheWritesPrice = model.pricing?.input_cache_write
+		? parseApiPrice(model.pricing?.input_cache_write)
+		: undefined
+
+	const cacheReadsPrice = model.pricing?.input_cache_read ? parseApiPrice(model.pricing?.input_cache_read) : undefined
+
+	const supportsPromptCache = typeof cacheWritesPrice !== "undefined" && typeof cacheReadsPrice !== "undefined"
+	const supportsImages =
+		VERCEL_AI_GATEWAY_VISION_ONLY_MODELS.has(id) || VERCEL_AI_GATEWAY_VISION_AND_TOOLS_MODELS.has(id)
+	const supportsComputerUse = VERCEL_AI_GATEWAY_VISION_AND_TOOLS_MODELS.has(id)
+
+	const modelInfo: ModelInfo = {
+		maxTokens: model.max_tokens,
+		contextWindow: model.context_window,
+		supportsImages,
+		supportsComputerUse,
+		supportsPromptCache,
+		inputPrice: parseApiPrice(model.pricing?.input),
+		outputPrice: parseApiPrice(model.pricing?.output),
+		cacheWritesPrice,
+		cacheReadsPrice,
+		description: model.description,
+	}
+
+	return modelInfo
+}

+ 1 - 0
src/api/providers/index.ts

@@ -32,3 +32,4 @@ export { ZAiHandler } from "./zai"
 export { FireworksHandler } from "./fireworks"
 export { RooHandler } from "./roo"
 export { FeatherlessHandler } from "./featherless"
+export { VercelAiGatewayHandler } from "./vercel-ai-gateway"

+ 115 - 0
src/api/providers/vercel-ai-gateway.ts

@@ -0,0 +1,115 @@
+import { Anthropic } from "@anthropic-ai/sdk"
+import OpenAI from "openai"
+
+import {
+	vercelAiGatewayDefaultModelId,
+	vercelAiGatewayDefaultModelInfo,
+	VERCEL_AI_GATEWAY_DEFAULT_TEMPERATURE,
+	VERCEL_AI_GATEWAY_PROMPT_CACHING_MODELS,
+} from "@roo-code/types"
+
+import { ApiHandlerOptions } from "../../shared/api"
+
+import { ApiStream } from "../transform/stream"
+import { convertToOpenAiMessages } from "../transform/openai-format"
+import { addCacheBreakpoints } from "../transform/caching/vercel-ai-gateway"
+
+import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
+import { RouterProvider } from "./router-provider"
+
+// Extend OpenAI's CompletionUsage to include Vercel AI Gateway specific fields
+interface VercelAiGatewayUsage extends OpenAI.CompletionUsage {
+	cache_creation_input_tokens?: number
+	cost?: number
+}
+
+export class VercelAiGatewayHandler extends RouterProvider implements SingleCompletionHandler {
+	constructor(options: ApiHandlerOptions) {
+		super({
+			options,
+			name: "vercel-ai-gateway",
+			baseURL: "https://ai-gateway.vercel.sh/v1",
+			apiKey: options.vercelAiGatewayApiKey,
+			modelId: options.vercelAiGatewayModelId,
+			defaultModelId: vercelAiGatewayDefaultModelId,
+			defaultModelInfo: vercelAiGatewayDefaultModelInfo,
+		})
+	}
+
+	override async *createMessage(
+		systemPrompt: string,
+		messages: Anthropic.Messages.MessageParam[],
+		metadata?: ApiHandlerCreateMessageMetadata,
+	): ApiStream {
+		const { id: modelId, info } = await this.fetchModel()
+
+		const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
+			{ role: "system", content: systemPrompt },
+			...convertToOpenAiMessages(messages),
+		]
+
+		if (VERCEL_AI_GATEWAY_PROMPT_CACHING_MODELS.has(modelId) && info.supportsPromptCache) {
+			addCacheBreakpoints(systemPrompt, openAiMessages)
+		}
+
+		const body: OpenAI.Chat.ChatCompletionCreateParams = {
+			model: modelId,
+			messages: openAiMessages,
+			temperature: this.supportsTemperature(modelId)
+				? (this.options.modelTemperature ?? VERCEL_AI_GATEWAY_DEFAULT_TEMPERATURE)
+				: undefined,
+			max_completion_tokens: info.maxTokens,
+			stream: true,
+		}
+
+		const completion = await this.client.chat.completions.create(body)
+
+		for await (const chunk of completion) {
+			const delta = chunk.choices[0]?.delta
+			if (delta?.content) {
+				yield {
+					type: "text",
+					text: delta.content,
+				}
+			}
+
+			if (chunk.usage) {
+				const usage = chunk.usage as VercelAiGatewayUsage
+				yield {
+					type: "usage",
+					inputTokens: usage.prompt_tokens || 0,
+					outputTokens: usage.completion_tokens || 0,
+					cacheWriteTokens: usage.cache_creation_input_tokens || undefined,
+					cacheReadTokens: usage.prompt_tokens_details?.cached_tokens || undefined,
+					totalCost: usage.cost ?? 0,
+				}
+			}
+		}
+	}
+
+	async completePrompt(prompt: string): Promise<string> {
+		const { id: modelId, info } = await this.fetchModel()
+
+		try {
+			const requestOptions: OpenAI.Chat.ChatCompletionCreateParams = {
+				model: modelId,
+				messages: [{ role: "user", content: prompt }],
+				stream: false,
+			}
+
+			if (this.supportsTemperature(modelId)) {
+				requestOptions.temperature = this.options.modelTemperature ?? VERCEL_AI_GATEWAY_DEFAULT_TEMPERATURE
+			}
+
+			requestOptions.max_completion_tokens = info.maxTokens
+
+			const response = await this.client.chat.completions.create(requestOptions)
+			return response.choices[0]?.message.content || ""
+		} catch (error) {
+			if (error instanceof Error) {
+				throw new Error(`Vercel AI Gateway completion error: ${error.message}`)
+			}
+			throw error
+		}
+	}
+}

+ 233 - 0
src/api/transform/caching/__tests__/vercel-ai-gateway.spec.ts

@@ -0,0 +1,233 @@
+// npx vitest run src/api/transform/caching/__tests__/vercel-ai-gateway.spec.ts
+
+import OpenAI from "openai"
+import { addCacheBreakpoints } from "../vercel-ai-gateway"
+
+describe("Vercel AI Gateway Caching", () => {
+	describe("addCacheBreakpoints", () => {
+		it("adds cache control to system message", () => {
+			const systemPrompt = "You are a helpful assistant."
+			const messages: OpenAI.Chat.ChatCompletionMessageParam[] = [
+				{ role: "system", content: systemPrompt },
+				{ role: "user", content: "Hello" },
+			]
+
+			addCacheBreakpoints(systemPrompt, messages)
+
+			expect(messages[0]).toEqual({
+				role: "system",
+				content: systemPrompt,
+				cache_control: { type: "ephemeral" },
+			})
+		})
+
+		it("adds cache control to last two user messages with string content", () => {
+			const systemPrompt = "You are a helpful assistant."
+			const messages: OpenAI.Chat.ChatCompletionMessageParam[] = [
+				{ role: "system", content: systemPrompt },
+				{ role: "user", content: "First message" },
+				{ role: "assistant", content: "First response" },
+				{ role: "user", content: "Second message" },
+				{ role: "assistant", content: "Second response" },
+				{ role: "user", content: "Third message" },
+				{ role: "assistant", content: "Third response" },
+				{ role: "user", content: "Fourth message" },
+			]
+
+			addCacheBreakpoints(systemPrompt, messages)
+
+			const lastUserMessage = messages[7]
+			expect(Array.isArray(lastUserMessage.content)).toBe(true)
+			if (Array.isArray(lastUserMessage.content)) {
+				const textPart = lastUserMessage.content.find((part) => part.type === "text")
+				expect(textPart).toEqual({
+					type: "text",
+					text: "Fourth message",
+					cache_control: { type: "ephemeral" },
+				})
+			}
+
+			const secondLastUserMessage = messages[5]
+			expect(Array.isArray(secondLastUserMessage.content)).toBe(true)
+			if (Array.isArray(secondLastUserMessage.content)) {
+				const textPart = secondLastUserMessage.content.find((part) => part.type === "text")
+				expect(textPart).toEqual({
+					type: "text",
+					text: "Third message",
+					cache_control: { type: "ephemeral" },
+				})
+			}
+		})
+
+		it("handles messages with existing array content", () => {
+			const systemPrompt = "You are a helpful assistant."
+			const messages: OpenAI.Chat.ChatCompletionMessageParam[] = [
+				{ role: "system", content: systemPrompt },
+				{
+					role: "user",
+					content: [
+						{ type: "text", text: "Hello with image" },
+						{ type: "image_url", image_url: { url: "data:image/png;base64,..." } },
+					],
+				},
+			]
+
+			addCacheBreakpoints(systemPrompt, messages)
+
+			const userMessage = messages[1]
+			expect(Array.isArray(userMessage.content)).toBe(true)
+			if (Array.isArray(userMessage.content)) {
+				const textPart = userMessage.content.find((part) => part.type === "text")
+				expect(textPart).toEqual({
+					type: "text",
+					text: "Hello with image",
+					cache_control: { type: "ephemeral" },
+				})
+
+				const imagePart = userMessage.content.find((part) => part.type === "image_url")
+				expect(imagePart).toEqual({
+					type: "image_url",
+					image_url: { url: "data:image/png;base64,..." },
+				})
+			}
+		})
+
+		it("handles empty string content gracefully", () => {
+			const systemPrompt = "You are a helpful assistant."
+			const messages: OpenAI.Chat.ChatCompletionMessageParam[] = [
+				{ role: "system", content: systemPrompt },
+				{ role: "user", content: "" },
+			]
+
+			addCacheBreakpoints(systemPrompt, messages)
+
+			const userMessage = messages[1]
+			expect(userMessage.content).toBe("")
+		})
+
+		it("handles messages with no text parts", () => {
+			const systemPrompt = "You are a helpful assistant."
+			const messages: OpenAI.Chat.ChatCompletionMessageParam[] = [
+				{ role: "system", content: systemPrompt },
+				{
+					role: "user",
+					content: [{ type: "image_url", image_url: { url: "data:image/png;base64,..." } }],
+				},
+			]
+
+			addCacheBreakpoints(systemPrompt, messages)
+
+			const userMessage = messages[1]
+			expect(Array.isArray(userMessage.content)).toBe(true)
+			if (Array.isArray(userMessage.content)) {
+				const textPart = userMessage.content.find((part) => part.type === "text")
+				expect(textPart).toBeUndefined()
+
+				const imagePart = userMessage.content.find((part) => part.type === "image_url")
+				expect(imagePart).toEqual({
+					type: "image_url",
+					image_url: { url: "data:image/png;base64,..." },
+				})
+			}
+		})
+
+		it("processes only user messages for conversation caching", () => {
+			const systemPrompt = "You are a helpful assistant."
+			const messages: OpenAI.Chat.ChatCompletionMessageParam[] = [
+				{ role: "system", content: systemPrompt },
+				{ role: "user", content: "First user" },
+				{ role: "assistant", content: "Assistant response" },
+				{ role: "user", content: "Second user" },
+			]
+
+			addCacheBreakpoints(systemPrompt, messages)
+
+			expect(messages[2]).toEqual({
+				role: "assistant",
+				content: "Assistant response",
+			})
+
+			const firstUser = messages[1]
+			const secondUser = messages[3]
+
+			expect(Array.isArray(firstUser.content)).toBe(true)
+			expect(Array.isArray(secondUser.content)).toBe(true)
+		})
+
+		it("handles case with only one user message", () => {
+			const systemPrompt = "You are a helpful assistant."
+			const messages: OpenAI.Chat.ChatCompletionMessageParam[] = [
+				{ role: "system", content: systemPrompt },
+				{ role: "user", content: "Only message" },
+			]
+
+			addCacheBreakpoints(systemPrompt, messages)
+
+			const userMessage = messages[1]
+			expect(Array.isArray(userMessage.content)).toBe(true)
+			if (Array.isArray(userMessage.content)) {
+				const textPart = userMessage.content.find((part) => part.type === "text")
+				expect(textPart).toEqual({
+					type: "text",
+					text: "Only message",
+					cache_control: { type: "ephemeral" },
+				})
+			}
+		})
+
+		it("handles case with no user messages", () => {
+			const systemPrompt = "You are a helpful assistant."
+			const messages: OpenAI.Chat.ChatCompletionMessageParam[] = [
+				{ role: "system", content: systemPrompt },
+				{ role: "assistant", content: "Assistant only" },
+			]
+
+			addCacheBreakpoints(systemPrompt, messages)
+
+			expect(messages[0]).toEqual({
+				role: "system",
+				content: systemPrompt,
+				cache_control: { type: "ephemeral" },
+			})
+
+			expect(messages[1]).toEqual({
+				role: "assistant",
+				content: "Assistant only",
+			})
+		})
+
+		it("handles messages with multiple text parts", () => {
+			const systemPrompt = "You are a helpful assistant."
+			const messages: OpenAI.Chat.ChatCompletionMessageParam[] = [
+				{ role: "system", content: systemPrompt },
+				{
+					role: "user",
+					content: [
+						{ type: "text", text: "First part" },
+						{ type: "image_url", image_url: { url: "data:image/png;base64,..." } },
+						{ type: "text", text: "Second part" },
+					],
+				},
+			]
+
+			addCacheBreakpoints(systemPrompt, messages)
+
+			const userMessage = messages[1]
+			if (Array.isArray(userMessage.content)) {
+				const textParts = userMessage.content.filter((part) => part.type === "text")
+				expect(textParts).toHaveLength(2)
+
+				expect(textParts[0]).toEqual({
+					type: "text",
+					text: "First part",
+				})
+
+				expect(textParts[1]).toEqual({
+					type: "text",
+					text: "Second part",
+					cache_control: { type: "ephemeral" },
+				})
+			}
+		})
+	})
+})

+ 30 - 0
src/api/transform/caching/vercel-ai-gateway.ts

@@ -0,0 +1,30 @@
+import OpenAI from "openai"
+
+export function addCacheBreakpoints(systemPrompt: string, messages: OpenAI.Chat.ChatCompletionMessageParam[]) {
+	// Apply cache_control to system message at the message level
+	messages[0] = {
+		role: "system",
+		content: systemPrompt,
+		// @ts-ignore-next-line
+		cache_control: { type: "ephemeral" },
+	}
+
+	// Add cache_control to the last two user messages for conversation context caching
+	const lastTwoUserMessages = messages.filter((msg) => msg.role === "user").slice(-2)
+
+	lastTwoUserMessages.forEach((msg) => {
+		if (typeof msg.content === "string" && msg.content.length > 0) {
+			msg.content = [{ type: "text", text: msg.content }]
+		}
+
+		if (Array.isArray(msg.content)) {
+			// Find the last text part in the message content
+			let lastTextPart = msg.content.filter((part) => part.type === "text").pop()
+
+			if (lastTextPart && lastTextPart.text && lastTextPart.text.length > 0) {
+				// @ts-ignore-next-line
+				lastTextPart["cache_control"] = { type: "ephemeral" }
+			}
+		}
+	})
+}

+ 5 - 0
src/core/webview/__tests__/ClineProvider.spec.ts

@@ -2669,6 +2669,7 @@ describe("ClineProvider - Router Models", () => {
 		expect(getModels).toHaveBeenCalledWith({ provider: "requesty", apiKey: "requesty-key" })
 		expect(getModels).toHaveBeenCalledWith({ provider: "glama" })
 		expect(getModels).toHaveBeenCalledWith({ provider: "unbound", apiKey: "unbound-key" })
+		expect(getModels).toHaveBeenCalledWith({ provider: "vercel-ai-gateway" })
 		expect(getModels).toHaveBeenCalledWith({
 			provider: "litellm",
 			apiKey: "litellm-key",
@@ -2686,6 +2687,7 @@ describe("ClineProvider - Router Models", () => {
 				litellm: mockModels,
 				ollama: {},
 				lmstudio: {},
+				"vercel-ai-gateway": mockModels,
 			},
 		})
 	})
@@ -2716,6 +2718,7 @@ describe("ClineProvider - Router Models", () => {
 			.mockRejectedValueOnce(new Error("Requesty API error")) // requesty fail
 			.mockResolvedValueOnce(mockModels) // glama success
 			.mockRejectedValueOnce(new Error("Unbound API error")) // unbound fail
+			.mockResolvedValueOnce(mockModels) // vercel-ai-gateway success
 			.mockRejectedValueOnce(new Error("LiteLLM connection failed")) // litellm fail
 
 		await messageHandler({ type: "requestRouterModels" })
@@ -2731,6 +2734,7 @@ describe("ClineProvider - Router Models", () => {
 				ollama: {},
 				lmstudio: {},
 				litellm: {},
+				"vercel-ai-gateway": mockModels,
 			},
 		})
 
@@ -2841,6 +2845,7 @@ describe("ClineProvider - Router Models", () => {
 				litellm: {},
 				ollama: {},
 				lmstudio: {},
+				"vercel-ai-gateway": mockModels,
 			},
 		})
 	})

+ 6 - 0
src/core/webview/__tests__/webviewMessageHandler.spec.ts

@@ -178,6 +178,7 @@ describe("webviewMessageHandler - requestRouterModels", () => {
 		expect(mockGetModels).toHaveBeenCalledWith({ provider: "requesty", apiKey: "requesty-key" })
 		expect(mockGetModels).toHaveBeenCalledWith({ provider: "glama" })
 		expect(mockGetModels).toHaveBeenCalledWith({ provider: "unbound", apiKey: "unbound-key" })
+		expect(mockGetModels).toHaveBeenCalledWith({ provider: "vercel-ai-gateway" })
 		expect(mockGetModels).toHaveBeenCalledWith({
 			provider: "litellm",
 			apiKey: "litellm-key",
@@ -195,6 +196,7 @@ describe("webviewMessageHandler - requestRouterModels", () => {
 				litellm: mockModels,
 				ollama: {},
 				lmstudio: {},
+				"vercel-ai-gateway": mockModels,
 			},
 		})
 	})
@@ -282,6 +284,7 @@ describe("webviewMessageHandler - requestRouterModels", () => {
 				litellm: {},
 				ollama: {},
 				lmstudio: {},
+				"vercel-ai-gateway": mockModels,
 			},
 		})
 	})
@@ -302,6 +305,7 @@ describe("webviewMessageHandler - requestRouterModels", () => {
 			.mockRejectedValueOnce(new Error("Requesty API error")) // requesty
 			.mockResolvedValueOnce(mockModels) // glama
 			.mockRejectedValueOnce(new Error("Unbound API error")) // unbound
+			.mockResolvedValueOnce(mockModels) // vercel-ai-gateway
 			.mockRejectedValueOnce(new Error("LiteLLM connection failed")) // litellm
 
 		await webviewMessageHandler(mockClineProvider, {
@@ -319,6 +323,7 @@ describe("webviewMessageHandler - requestRouterModels", () => {
 				litellm: {},
 				ollama: {},
 				lmstudio: {},
+				"vercel-ai-gateway": mockModels,
 			},
 		})
 
@@ -352,6 +357,7 @@ describe("webviewMessageHandler - requestRouterModels", () => {
 			.mockRejectedValueOnce(new Error("Requesty API error")) // requesty
 			.mockRejectedValueOnce(new Error("Glama API error")) // glama
 			.mockRejectedValueOnce(new Error("Unbound API error")) // unbound
+			.mockRejectedValueOnce(new Error("Vercel AI Gateway error")) // vercel-ai-gateway
 			.mockRejectedValueOnce(new Error("LiteLLM connection failed")) // litellm
 
 		await webviewMessageHandler(mockClineProvider, {

+ 1 - 0
src/core/webview/webviewMessageHandler.ts

@@ -575,6 +575,7 @@ export const webviewMessageHandler = async (
 				},
 				{ key: "glama", options: { provider: "glama" } },
 				{ key: "unbound", options: { provider: "unbound", apiKey: apiConfiguration.unboundApiKey } },
+				{ key: "vercel-ai-gateway", options: { provider: "vercel-ai-gateway" } },
 			]
 
 			// Add IO Intelligence if API key is provided

+ 1 - 1
src/package.json

@@ -429,7 +429,7 @@
 		"@mistralai/mistralai": "^1.9.18",
 		"@modelcontextprotocol/sdk": "^1.9.0",
 		"@qdrant/js-client-rest": "^1.14.0",
-		"@roo-code/cloud": "^0.21.0",
+		"@roo-code/cloud": "^0.22.0",
 		"@roo-code/ipc": "workspace:^",
 		"@roo-code/telemetry": "workspace:^",
 		"@roo-code/types": "workspace:^",

+ 2 - 0
src/shared/api.ts

@@ -27,6 +27,7 @@ const routerNames = [
 	"ollama",
 	"lmstudio",
 	"io-intelligence",
+	"vercel-ai-gateway",
 ] as const
 
 export type RouterName = (typeof routerNames)[number]
@@ -151,3 +152,4 @@ export type GetModelsOptions =
 	| { provider: "ollama"; baseUrl?: string }
 	| { provider: "lmstudio"; baseUrl?: string }
 	| { provider: "io-intelligence"; apiKey: string }
+	| { provider: "vercel-ai-gateway" }

+ 13 - 0
webview-ui/src/components/settings/ApiOptions.tsx

@@ -35,6 +35,7 @@ import {
 	featherlessDefaultModelId,
 	ioIntelligenceDefaultModelId,
 	rooDefaultModelId,
+	vercelAiGatewayDefaultModelId,
 } from "@roo-code/types"
 
 import { vscode } from "@src/utils/vscode"
@@ -91,6 +92,7 @@ import {
 	ZAi,
 	Fireworks,
 	Featherless,
+	VercelAiGateway,
 } from "./providers"
 
 import { MODELS_BY_PROVIDER, PROVIDERS } from "./constants"
@@ -335,6 +337,7 @@ const ApiOptions = ({
 				featherless: { field: "apiModelId", default: featherlessDefaultModelId },
 				"io-intelligence": { field: "ioIntelligenceModelId", default: ioIntelligenceDefaultModelId },
 				roo: { field: "apiModelId", default: rooDefaultModelId },
+				"vercel-ai-gateway": { field: "vercelAiGatewayModelId", default: vercelAiGatewayDefaultModelId },
 				openai: { field: "openAiModelId" },
 				ollama: { field: "ollamaModelId" },
 				lmstudio: { field: "lmStudioModelId" },
@@ -607,6 +610,16 @@ const ApiOptions = ({
 				/>
 			)}
 
+			{selectedProvider === "vercel-ai-gateway" && (
+				<VercelAiGateway
+					apiConfiguration={apiConfiguration}
+					setApiConfigurationField={setApiConfigurationField}
+					routerModels={routerModels}
+					organizationAllowList={organizationAllowList}
+					modelValidationError={modelValidationError}
+				/>
+			)}
+
 			{selectedProvider === "human-relay" && (
 				<>
 					<div className="text-sm text-vscode-descriptionForeground">

+ 1 - 0
webview-ui/src/components/settings/ModelPicker.tsx

@@ -37,6 +37,7 @@ type ModelIdKey = keyof Pick<
 	| "openAiModelId"
 	| "litellmModelId"
 	| "ioIntelligenceModelId"
+	| "vercelAiGatewayModelId"
 >
 
 interface ModelPickerProps {

+ 1 - 0
webview-ui/src/components/settings/constants.ts

@@ -79,4 +79,5 @@ export const PROVIDERS = [
 	{ value: "featherless", label: "Featherless AI" },
 	{ value: "io-intelligence", label: "IO Intelligence" },
 	{ value: "roo", label: "Roo Code Cloud" },
+	{ value: "vercel-ai-gateway", label: "Vercel AI Gateway" },
 ].sort((a, b) => a.label.localeCompare(b.label))

+ 77 - 0
webview-ui/src/components/settings/providers/VercelAiGateway.tsx

@@ -0,0 +1,77 @@
+import { useCallback } from "react"
+import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
+
+import { type ProviderSettings, vercelAiGatewayDefaultModelId } from "@roo-code/types"
+
+import type { OrganizationAllowList } from "@roo/cloud"
+import type { RouterModels } from "@roo/api"
+
+import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
+
+import { inputEventTransform } from "../transforms"
+import { ModelPicker } from "../ModelPicker"
+
+type VercelAiGatewayProps = {
+	apiConfiguration: ProviderSettings
+	setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void
+	routerModels?: RouterModels
+	organizationAllowList: OrganizationAllowList
+	modelValidationError?: string
+}
+
+export const VercelAiGateway = ({
+	apiConfiguration,
+	setApiConfigurationField,
+	routerModels,
+	organizationAllowList,
+	modelValidationError,
+}: VercelAiGatewayProps) => {
+	const { t } = useAppTranslation()
+
+	const handleInputChange = useCallback(
+		<K extends keyof ProviderSettings, E>(
+			field: K,
+			transform: (event: E) => ProviderSettings[K] = inputEventTransform,
+		) =>
+			(event: E | Event) => {
+				setApiConfigurationField(field, transform(event as E))
+			},
+		[setApiConfigurationField],
+	)
+
+	return (
+		<>
+			<VSCodeTextField
+				value={apiConfiguration?.vercelAiGatewayApiKey || ""}
+				type="password"
+				onInput={handleInputChange("vercelAiGatewayApiKey")}
+				placeholder={t("settings:placeholders.apiKey")}
+				className="w-full">
+				<label className="block font-medium mb-1">{t("settings:providers.vercelAiGatewayApiKey")}</label>
+			</VSCodeTextField>
+			<div className="text-sm text-vscode-descriptionForeground -mt-2">
+				{t("settings:providers.apiKeyStorageNotice")}
+			</div>
+			{!apiConfiguration?.vercelAiGatewayApiKey && (
+				<VSCodeButtonLink
+					href="https://vercel.com/d?to=%2F%5Bteam%5D%2F%7E%2Fai%2Fapi-keys&title=AI+Gateway+API+Key"
+					appearance="primary"
+					style={{ width: "100%" }}>
+					{t("settings:providers.getVercelAiGatewayApiKey")}
+				</VSCodeButtonLink>
+			)}
+			<ModelPicker
+				apiConfiguration={apiConfiguration}
+				setApiConfigurationField={setApiConfigurationField}
+				defaultModelId={vercelAiGatewayDefaultModelId}
+				models={routerModels?.["vercel-ai-gateway"] ?? {}}
+				modelIdKey="vercelAiGatewayModelId"
+				serviceName="Vercel AI Gateway"
+				serviceUrl="https://vercel.com/ai-gateway/models"
+				organizationAllowList={organizationAllowList}
+				errorMessage={modelValidationError}
+			/>
+		</>
+	)
+}

+ 1 - 0
webview-ui/src/components/settings/providers/index.ts

@@ -28,3 +28,4 @@ export { ZAi } from "./ZAi"
 export { LiteLLM } from "./LiteLLM"
 export { Fireworks } from "./Fireworks"
 export { Featherless } from "./Featherless"
+export { VercelAiGateway } from "./VercelAiGateway"

+ 6 - 0
webview-ui/src/components/ui/hooks/useSelectedModel.ts

@@ -54,6 +54,7 @@ import {
 	rooModels,
 	qwenCodeDefaultModelId,
 	qwenCodeModels,
+	vercelAiGatewayDefaultModelId,
 	BEDROCK_CLAUDE_SONNET_4_MODEL_ID,
 } from "@roo-code/types"
 
@@ -329,6 +330,11 @@ function getSelectedModel({
 			const info = qwenCodeModels[id as keyof typeof qwenCodeModels]
 			return { id, info }
 		}
+		case "vercel-ai-gateway": {
+			const id = apiConfiguration.vercelAiGatewayModelId ?? vercelAiGatewayDefaultModelId
+			const info = routerModels["vercel-ai-gateway"]?.[id]
+			return { id, info }
+		}
 		// case "anthropic":
 		// case "human-relay":
 		// case "fake-ai":

+ 2 - 0
webview-ui/src/i18n/locales/ca/settings.json

@@ -233,6 +233,8 @@
 		"awsCustomArnDesc": "Assegureu-vos que la regió a l'ARN coincideix amb la regió d'AWS seleccionada anteriorment.",
 		"openRouterApiKey": "Clau API d'OpenRouter",
 		"getOpenRouterApiKey": "Obtenir clau API d'OpenRouter",
+		"vercelAiGatewayApiKey": "Clau API de Vercel AI Gateway",
+		"getVercelAiGatewayApiKey": "Obtenir clau API de Vercel AI Gateway",
 		"apiKeyStorageNotice": "Les claus API s'emmagatzemen de forma segura a l'Emmagatzematge Secret de VSCode",
 		"glamaApiKey": "Clau API de Glama",
 		"getGlamaApiKey": "Obtenir clau API de Glama",

+ 2 - 0
webview-ui/src/i18n/locales/de/settings.json

@@ -233,6 +233,8 @@
 		"awsCustomArnDesc": "Stellen Sie sicher, dass die Region in der ARN mit Ihrer oben ausgewählten AWS-Region übereinstimmt.",
 		"openRouterApiKey": "OpenRouter API-Schlüssel",
 		"getOpenRouterApiKey": "OpenRouter API-Schlüssel erhalten",
+		"vercelAiGatewayApiKey": "Vercel AI Gateway API-Schlüssel",
+		"getVercelAiGatewayApiKey": "Vercel AI Gateway API-Schlüssel erhalten",
 		"doubaoApiKey": "Doubao API-Schlüssel",
 		"getDoubaoApiKey": "Doubao API-Schlüssel erhalten",
 		"apiKeyStorageNotice": "API-Schlüssel werden sicher im VSCode Secret Storage gespeichert",

+ 2 - 0
webview-ui/src/i18n/locales/en/settings.json

@@ -232,6 +232,8 @@
 		"awsCustomArnDesc": "Make sure the region in the ARN matches your selected AWS Region above.",
 		"openRouterApiKey": "OpenRouter API Key",
 		"getOpenRouterApiKey": "Get OpenRouter API Key",
+		"vercelAiGatewayApiKey": "Vercel AI Gateway API Key",
+		"getVercelAiGatewayApiKey": "Get Vercel AI Gateway API Key",
 		"apiKeyStorageNotice": "API keys are stored securely in VSCode's Secret Storage",
 		"glamaApiKey": "Glama API Key",
 		"getGlamaApiKey": "Get Glama API Key",

+ 2 - 0
webview-ui/src/i18n/locales/es/settings.json

@@ -233,6 +233,8 @@
 		"awsCustomArnDesc": "Asegúrese de que la región en el ARN coincida con la región de AWS seleccionada anteriormente.",
 		"openRouterApiKey": "Clave API de OpenRouter",
 		"getOpenRouterApiKey": "Obtener clave API de OpenRouter",
+		"vercelAiGatewayApiKey": "Clave API de Vercel AI Gateway",
+		"getVercelAiGatewayApiKey": "Obtener clave API de Vercel AI Gateway",
 		"apiKeyStorageNotice": "Las claves API se almacenan de forma segura en el Almacenamiento Secreto de VSCode",
 		"glamaApiKey": "Clave API de Glama",
 		"getGlamaApiKey": "Obtener clave API de Glama",

+ 2 - 0
webview-ui/src/i18n/locales/fr/settings.json

@@ -233,6 +233,8 @@
 		"awsCustomArnDesc": "Assurez-vous que la région dans l'ARN correspond à la région AWS sélectionnée ci-dessus.",
 		"openRouterApiKey": "Clé API OpenRouter",
 		"getOpenRouterApiKey": "Obtenir la clé API OpenRouter",
+		"vercelAiGatewayApiKey": "Clé API Vercel AI Gateway",
+		"getVercelAiGatewayApiKey": "Obtenir la clé API Vercel AI Gateway",
 		"apiKeyStorageNotice": "Les clés API sont stockées en toute sécurité dans le stockage sécurisé de VSCode",
 		"glamaApiKey": "Clé API Glama",
 		"getGlamaApiKey": "Obtenir la clé API Glama",

+ 2 - 0
webview-ui/src/i18n/locales/hi/settings.json

@@ -233,6 +233,8 @@
 		"awsCustomArnDesc": "सुनिश्चित करें कि ARN में क्षेत्र ऊपर चयनित AWS क्षेत्र से मेल खाता है।",
 		"openRouterApiKey": "OpenRouter API कुंजी",
 		"getOpenRouterApiKey": "OpenRouter API कुंजी प्राप्त करें",
+		"vercelAiGatewayApiKey": "Vercel AI Gateway API कुंजी",
+		"getVercelAiGatewayApiKey": "Vercel AI Gateway API कुंजी प्राप्त करें",
 		"apiKeyStorageNotice": "API कुंजियाँ VSCode के सुरक्षित स्टोरेज में सुरक्षित रूप से संग्रहीत हैं",
 		"glamaApiKey": "Glama API कुंजी",
 		"getGlamaApiKey": "Glama API कुंजी प्राप्त करें",

+ 2 - 0
webview-ui/src/i18n/locales/id/settings.json

@@ -237,6 +237,8 @@
 		"awsCustomArnDesc": "Pastikan region di ARN cocok dengan AWS Region yang kamu pilih di atas.",
 		"openRouterApiKey": "OpenRouter API Key",
 		"getOpenRouterApiKey": "Dapatkan OpenRouter API Key",
+		"vercelAiGatewayApiKey": "Vercel AI Gateway API Key",
+		"getVercelAiGatewayApiKey": "Dapatkan Vercel AI Gateway API Key",
 		"apiKeyStorageNotice": "API key disimpan dengan aman di Secret Storage VSCode",
 		"glamaApiKey": "Glama API Key",
 		"getGlamaApiKey": "Dapatkan Glama API Key",

+ 2 - 0
webview-ui/src/i18n/locales/it/settings.json

@@ -233,6 +233,8 @@
 		"awsCustomArnDesc": "Assicurati che la regione nell'ARN corrisponda alla regione AWS selezionata sopra.",
 		"openRouterApiKey": "Chiave API OpenRouter",
 		"getOpenRouterApiKey": "Ottieni chiave API OpenRouter",
+		"vercelAiGatewayApiKey": "Chiave API Vercel AI Gateway",
+		"getVercelAiGatewayApiKey": "Ottieni chiave API Vercel AI Gateway",
 		"apiKeyStorageNotice": "Le chiavi API sono memorizzate in modo sicuro nell'Archivio Segreto di VSCode",
 		"glamaApiKey": "Chiave API Glama",
 		"getGlamaApiKey": "Ottieni chiave API Glama",

+ 2 - 0
webview-ui/src/i18n/locales/ja/settings.json

@@ -233,6 +233,8 @@
 		"awsCustomArnDesc": "ARN内のリージョンが上で選択したAWSリージョンと一致していることを確認してください。",
 		"openRouterApiKey": "OpenRouter APIキー",
 		"getOpenRouterApiKey": "OpenRouter APIキーを取得",
+		"vercelAiGatewayApiKey": "Vercel AI Gateway APIキー",
+		"getVercelAiGatewayApiKey": "Vercel AI Gateway APIキーを取得",
 		"apiKeyStorageNotice": "APIキーはVSCodeのシークレットストレージに安全に保存されます",
 		"glamaApiKey": "Glama APIキー",
 		"getGlamaApiKey": "Glama APIキーを取得",

+ 2 - 0
webview-ui/src/i18n/locales/ko/settings.json

@@ -233,6 +233,8 @@
 		"awsCustomArnDesc": "ARN의 리전이 위에서 선택한 AWS 리전과 일치하는지 확인하세요.",
 		"openRouterApiKey": "OpenRouter API 키",
 		"getOpenRouterApiKey": "OpenRouter API 키 받기",
+		"vercelAiGatewayApiKey": "Vercel AI Gateway API 키",
+		"getVercelAiGatewayApiKey": "Vercel AI Gateway API 키 받기",
 		"apiKeyStorageNotice": "API 키는 VSCode의 보안 저장소에 안전하게 저장됩니다",
 		"glamaApiKey": "Glama API 키",
 		"getGlamaApiKey": "Glama API 키 받기",

+ 2 - 0
webview-ui/src/i18n/locales/nl/settings.json

@@ -233,6 +233,8 @@
 		"awsCustomArnDesc": "Zorg ervoor dat de regio in de ARN overeenkomt met je geselecteerde AWS-regio hierboven.",
 		"openRouterApiKey": "OpenRouter API-sleutel",
 		"getOpenRouterApiKey": "OpenRouter API-sleutel ophalen",
+		"vercelAiGatewayApiKey": "Vercel AI Gateway API-sleutel",
+		"getVercelAiGatewayApiKey": "Vercel AI Gateway API-sleutel ophalen",
 		"apiKeyStorageNotice": "API-sleutels worden veilig opgeslagen in de geheime opslag van VSCode",
 		"glamaApiKey": "Glama API-sleutel",
 		"getGlamaApiKey": "Glama API-sleutel ophalen",

+ 2 - 0
webview-ui/src/i18n/locales/pl/settings.json

@@ -233,6 +233,8 @@
 		"awsCustomArnDesc": "Upewnij się, że region w ARN odpowiada wybranemu powyżej regionowi AWS.",
 		"openRouterApiKey": "Klucz API OpenRouter",
 		"getOpenRouterApiKey": "Uzyskaj klucz API OpenRouter",
+		"vercelAiGatewayApiKey": "Klucz API Vercel AI Gateway",
+		"getVercelAiGatewayApiKey": "Uzyskaj klucz API Vercel AI Gateway",
 		"apiKeyStorageNotice": "Klucze API są bezpiecznie przechowywane w Tajnym Magazynie VSCode",
 		"glamaApiKey": "Klucz API Glama",
 		"getGlamaApiKey": "Uzyskaj klucz API Glama",

+ 2 - 0
webview-ui/src/i18n/locales/pt-BR/settings.json

@@ -233,6 +233,8 @@
 		"awsCustomArnDesc": "Certifique-se de que a região no ARN corresponde à região AWS selecionada acima.",
 		"openRouterApiKey": "Chave de API OpenRouter",
 		"getOpenRouterApiKey": "Obter chave de API OpenRouter",
+		"vercelAiGatewayApiKey": "Chave API do Vercel AI Gateway",
+		"getVercelAiGatewayApiKey": "Obter chave API do Vercel AI Gateway",
 		"apiKeyStorageNotice": "As chaves de API são armazenadas com segurança no Armazenamento Secreto do VSCode",
 		"glamaApiKey": "Chave de API Glama",
 		"getGlamaApiKey": "Obter chave de API Glama",

+ 2 - 0
webview-ui/src/i18n/locales/ru/settings.json

@@ -233,6 +233,8 @@
 		"awsCustomArnDesc": "Убедитесь, что регион в ARN совпадает с выбранным выше регионом AWS.",
 		"openRouterApiKey": "OpenRouter API-ключ",
 		"getOpenRouterApiKey": "Получить OpenRouter API-ключ",
+		"vercelAiGatewayApiKey": "Ключ API Vercel AI Gateway",
+		"getVercelAiGatewayApiKey": "Получить ключ API Vercel AI Gateway",
 		"apiKeyStorageNotice": "API-ключи хранятся безопасно в Secret Storage VSCode",
 		"glamaApiKey": "Glama API-ключ",
 		"getGlamaApiKey": "Получить Glama API-ключ",

+ 2 - 0
webview-ui/src/i18n/locales/tr/settings.json

@@ -233,6 +233,8 @@
 		"awsCustomArnDesc": "ARN içindeki bölgenin yukarıda seçilen AWS Bölgesiyle eşleştiğinden emin olun.",
 		"openRouterApiKey": "OpenRouter API Anahtarı",
 		"getOpenRouterApiKey": "OpenRouter API Anahtarı Al",
+		"vercelAiGatewayApiKey": "Vercel AI Gateway API Anahtarı",
+		"getVercelAiGatewayApiKey": "Vercel AI Gateway API Anahtarı Al",
 		"apiKeyStorageNotice": "API anahtarları VSCode'un Gizli Depolamasında güvenli bir şekilde saklanır",
 		"glamaApiKey": "Glama API Anahtarı",
 		"getGlamaApiKey": "Glama API Anahtarı Al",

+ 2 - 0
webview-ui/src/i18n/locales/vi/settings.json

@@ -233,6 +233,8 @@
 		"awsCustomArnDesc": "Đảm bảo rằng vùng trong ARN khớp với vùng AWS đã chọn ở trên.",
 		"openRouterApiKey": "Khóa API OpenRouter",
 		"getOpenRouterApiKey": "Lấy khóa API OpenRouter",
+		"vercelAiGatewayApiKey": "Khóa API Vercel AI Gateway",
+		"getVercelAiGatewayApiKey": "Lấy khóa API Vercel AI Gateway",
 		"apiKeyStorageNotice": "Khóa API được lưu trữ an toàn trong Bộ lưu trữ bí mật của VSCode",
 		"glamaApiKey": "Khóa API Glama",
 		"getGlamaApiKey": "Lấy khóa API Glama",

+ 2 - 0
webview-ui/src/i18n/locales/zh-CN/settings.json

@@ -233,6 +233,8 @@
 		"awsCustomArnDesc": "请确保ARN中的区域与上方选择的AWS区域一致。",
 		"openRouterApiKey": "OpenRouter API 密钥",
 		"getOpenRouterApiKey": "获取 OpenRouter API 密钥",
+		"vercelAiGatewayApiKey": "Vercel AI Gateway API 密钥",
+		"getVercelAiGatewayApiKey": "获取 Vercel AI Gateway API 密钥",
 		"apiKeyStorageNotice": "API 密钥安全存储在 VSCode 的密钥存储中",
 		"glamaApiKey": "Glama API 密钥",
 		"getGlamaApiKey": "获取 Glama API 密钥",

+ 2 - 0
webview-ui/src/i18n/locales/zh-TW/settings.json

@@ -233,6 +233,8 @@
 		"awsCustomArnDesc": "確保 ARN 中的區域與您上面選擇的 AWS 區域相符。",
 		"openRouterApiKey": "OpenRouter API 金鑰",
 		"getOpenRouterApiKey": "取得 OpenRouter API 金鑰",
+		"vercelAiGatewayApiKey": "Vercel AI Gateway API 金鑰",
+		"getVercelAiGatewayApiKey": "取得 Vercel AI Gateway API 金鑰",
 		"apiKeyStorageNotice": "API 金鑰安全儲存於 VSCode 金鑰儲存中",
 		"glamaApiKey": "Glama API 金鑰",
 		"getGlamaApiKey": "取得 Glama API 金鑰",

+ 1 - 0
webview-ui/src/utils/__tests__/validate.test.ts

@@ -41,6 +41,7 @@ describe("Model Validation Functions", () => {
 		ollama: {},
 		lmstudio: {},
 		"io-intelligence": {},
+		"vercel-ai-gateway": {},
 	}
 
 	const allowAllOrganization: OrganizationAllowList = {

+ 10 - 0
webview-ui/src/utils/validate.ts

@@ -136,6 +136,11 @@ function validateModelsAndKeysProvided(apiConfiguration: ProviderSettings): stri
 				return i18next.t("settings:validation.qwenCodeOauthPath")
 			}
 			break
+		case "vercel-ai-gateway":
+			if (!apiConfiguration.vercelAiGatewayApiKey) {
+				return i18next.t("settings:validation.apiKey")
+			}
+			break
 	}
 
 	return undefined
@@ -204,6 +209,8 @@ function getModelIdForProvider(apiConfiguration: ProviderSettings, provider: str
 			return apiConfiguration.huggingFaceModelId
 		case "io-intelligence":
 			return apiConfiguration.ioIntelligenceModelId
+		case "vercel-ai-gateway":
+			return apiConfiguration.vercelAiGatewayModelId
 		default:
 			return apiConfiguration.apiModelId
 	}
@@ -277,6 +284,9 @@ export function validateModelId(apiConfiguration: ProviderSettings, routerModels
 		case "io-intelligence":
 			modelId = apiConfiguration.ioIntelligenceModelId
 			break
+		case "vercel-ai-gateway":
+			modelId = apiConfiguration.vercelAiGatewayModelId
+			break
 	}
 
 	if (!modelId) {