From 3169f25a5c0108d6909cd9cb0d0c0bfb46afe0db Mon Sep 17 00:00:00 2001 From: joshualipman123 Date: Mon, 25 Aug 2025 11:33:35 -0700 Subject: [PATCH 01/13] feat: Add Vercel AI Gateway provider integration --- packages/types/src/global-settings.ts | 1 + packages/types/src/provider-settings.ts | 9 ++ packages/types/src/providers/index.ts | 1 + .../types/src/providers/vercel-ai-gateway.ts | 19 +++ src/api/index.ts | 3 + src/api/providers/fetchers/modelCache.ts | 4 + .../providers/fetchers/vercel-ai-gateway.ts | 115 ++++++++++++++++++ src/api/providers/index.ts | 1 + src/api/providers/vercel-ai-gateway.ts | 115 ++++++++++++++++++ src/assets/images/vercel-ai-gateway.png | Bin 0 -> 875 bytes src/core/webview/webviewMessageHandler.ts | 1 + src/shared/api.ts | 2 + .../src/components/settings/ApiOptions.tsx | 13 ++ .../src/components/settings/ModelPicker.tsx | 1 + .../src/components/settings/constants.ts | 1 + .../settings/providers/VercelAiGateway.tsx | 68 +++++++++++ .../components/settings/providers/index.ts | 1 + .../components/ui/hooks/useSelectedModel.ts | 6 + .../src/components/welcome/WelcomeView.tsx | 8 ++ webview-ui/src/i18n/locales/en/welcome.json | 4 + .../src/utils/__tests__/validate.test.ts | 1 + webview-ui/src/utils/validate.ts | 10 ++ 22 files changed, 384 insertions(+) create mode 100644 packages/types/src/providers/vercel-ai-gateway.ts create mode 100644 src/api/providers/fetchers/vercel-ai-gateway.ts create mode 100644 src/api/providers/vercel-ai-gateway.ts create mode 100644 src/assets/images/vercel-ai-gateway.png create mode 100644 webview-ui/src/components/settings/providers/VercelAiGateway.tsx diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index dd72d72fe959..3b80bba3b2de 100644 --- a/packages/types/src/global-settings.ts +++ b/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 diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index 4f5ca9c3caf3..ee301eaa6fb9 100644 --- a/packages/types/src/provider-settings.ts +++ b/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[] = [ "litellmModelId", "huggingFaceModelId", "ioIntelligenceModelId", + "vercelAiGatewayModelId", ] export const getModelId = (settings: ProviderSettings): string | undefined => { diff --git a/packages/types/src/providers/index.ts b/packages/types/src/providers/index.ts index 27951f0f1a48..97fa10ca8248 100644 --- a/packages/types/src/providers/index.ts +++ b/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" diff --git a/packages/types/src/providers/vercel-ai-gateway.ts b/packages/types/src/providers/vercel-ai-gateway.ts new file mode 100644 index 000000000000..837baf7c520a --- /dev/null +++ b/packages/types/src/providers/vercel-ai-gateway.ts @@ -0,0 +1,19 @@ +import type { ModelInfo } from "../model.js" + +// https://ai-gateway.vercel.sh/v1/ +export const vercelAiGatewayDefaultModelId = "anthropic/claude-sonnet-4" + +export const vercelAiGatewayDefaultModelInfo: ModelInfo = { + maxTokens: 64000, + contextWindow: 128000, + supportsImages: 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 diff --git a/src/api/index.ts b/src/api/index.ts index 188cb3930faf..59ca681f7967 100644 --- a/src/api/index.ts +++ b/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) diff --git a/src/api/providers/fetchers/modelCache.ts b/src/api/providers/fetchers/modelCache.ts index f4c240a61cf7..0005e8205f63 100644 --- a/src/api/providers/fetchers/modelCache.ts +++ b/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 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 diff --git a/src/api/providers/fetchers/vercel-ai-gateway.ts b/src/api/providers/fetchers/vercel-ai-gateway.ts new file mode 100644 index 000000000000..4444a3ec40e4 --- /dev/null +++ b/src/api/providers/fetchers/vercel-ai-gateway.ts @@ -0,0 +1,115 @@ +import axios from "axios" +import { z } from "zod" + +import type { ModelInfo } 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 + +/** + * VercelAiGatewayModelsResponse + */ + +const vercelAiGatewayModelsResponseSchema = z.object({ + object: z.string(), + data: z.array(vercelAiGatewayModelSchema), +}) + +type VercelAiGatewayModelsResponse = z.infer + +/** + * getVercelAiGatewayModels + */ + +export async function getVercelAiGatewayModels(options?: ApiHandlerOptions): Promise> { + const models: Record = {} + const baseURL = "https://ai-gateway.vercel.sh/v1" + + try { + const response = await axios.get(`${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 + + // Filter out embedding models (models with "embedding" in name) + if (id.toLowerCase().includes("embed")) { + 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 modelInfo: ModelInfo = { + maxTokens: model.max_tokens, + contextWindow: model.context_window, + supportsImages: false, + supportsPromptCache, + inputPrice: parseApiPrice(model.pricing?.input), + outputPrice: parseApiPrice(model.pricing?.output), + cacheWritesPrice, + cacheReadsPrice, + description: model.description, + } + + return modelInfo +} diff --git a/src/api/providers/index.ts b/src/api/providers/index.ts index e2b9047dfc96..c3786c5f56d7 100644 --- a/src/api/providers/index.ts +++ b/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" diff --git a/src/api/providers/vercel-ai-gateway.ts b/src/api/providers/vercel-ai-gateway.ts new file mode 100644 index 000000000000..87fb7bab9c36 --- /dev/null +++ b/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, +} 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/anthropic" + +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 + prompt_tokens_details?: { + cached_tokens?: 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 (modelId.startsWith("anthropic/claude-3")) { + addCacheBreakpoints(systemPrompt, openAiMessages) + } //TODO: add cache breakpoints for other models + + const body: OpenAI.Chat.ChatCompletionCreateParams = { + model: modelId, + messages: openAiMessages, + temperature: this.supportsTemperature(modelId) + ? (this.options.modelTemperature ?? VERCEL_AI_GATEWAY_DEFAULT_TEMPERATURE) + : undefined, + max_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, + } + } + } + } + + async completePrompt(prompt: string): Promise { + 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_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 + } + } +} diff --git a/src/assets/images/vercel-ai-gateway.png b/src/assets/images/vercel-ai-gateway.png new file mode 100644 index 0000000000000000000000000000000000000000..0c85df1fdecbfc4affad35e6cadf4664bbb0c98b GIT binary patch literal 875 zcmV-x1C;!UP)Z z88Aw^xE9+eO_s}U>EarU2R`6O*jHeRY<+o-;zyQCr)+&$<^VtOD=Y&nlpG;RH-2Tg z+yqjBJ49Gb7+EeYa#O-n1w1CKuvfrnsX?J>Bdjc!eNux$QV)DWMp!Q}LuOVuP9P)8 z<&4a%Fw6%AkQJ5%mPw5BO$xY%ysRwn0H_Q<(Jvd&k>#>E{6v2YKE2jT4^S6)r#JSZ zE6e3j=%GHC27E_fRue`a?!w0pSeqP9~^|W1_p-6MvspEP(^gtuRShA-U4Hi zib(oQVjD^qd;++?7+mr7hV?{u``Vv`km& )} + {selectedProvider === "vercel-ai-gateway" && ( + + )} + {selectedProvider === "human-relay" && ( <>
diff --git a/webview-ui/src/components/settings/ModelPicker.tsx b/webview-ui/src/components/settings/ModelPicker.tsx index 0753f9fc2bde..e398a9f01fba 100644 --- a/webview-ui/src/components/settings/ModelPicker.tsx +++ b/webview-ui/src/components/settings/ModelPicker.tsx @@ -37,6 +37,7 @@ type ModelIdKey = keyof Pick< | "openAiModelId" | "litellmModelId" | "ioIntelligenceModelId" + | "vercelAiGatewayModelId" > interface ModelPickerProps { diff --git a/webview-ui/src/components/settings/constants.ts b/webview-ui/src/components/settings/constants.ts index 1a94c5c59945..9aa02bbf53ca 100644 --- a/webview-ui/src/components/settings/constants.ts +++ b/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)) diff --git a/webview-ui/src/components/settings/providers/VercelAiGateway.tsx b/webview-ui/src/components/settings/providers/VercelAiGateway.tsx new file mode 100644 index 000000000000..edbcc831892d --- /dev/null +++ b/webview-ui/src/components/settings/providers/VercelAiGateway.tsx @@ -0,0 +1,68 @@ +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 { 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( + ( + field: K, + transform: (event: E) => ProviderSettings[K] = inputEventTransform, + ) => + (event: E | Event) => { + setApiConfigurationField(field, transform(event as E)) + }, + [setApiConfigurationField], + ) + + return ( + <> + + + +
+ {t("settings:providers.apiKeyStorageNotice")} +
+ + + ) +} diff --git a/webview-ui/src/components/settings/providers/index.ts b/webview-ui/src/components/settings/providers/index.ts index 46fea622c91a..eedbba0c2903 100644 --- a/webview-ui/src/components/settings/providers/index.ts +++ b/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" diff --git a/webview-ui/src/components/ui/hooks/useSelectedModel.ts b/webview-ui/src/components/ui/hooks/useSelectedModel.ts index aeb90c3491a4..b41222585f1d 100644 --- a/webview-ui/src/components/ui/hooks/useSelectedModel.ts +++ b/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" @@ -317,6 +318,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": diff --git a/webview-ui/src/components/welcome/WelcomeView.tsx b/webview-ui/src/components/welcome/WelcomeView.tsx index 215a290d0da9..40a1467e1216 100644 --- a/webview-ui/src/components/welcome/WelcomeView.tsx +++ b/webview-ui/src/components/welcome/WelcomeView.tsx @@ -83,6 +83,14 @@ const WelcomeView = () => { description: t("welcome:routers.openrouter.description"), authUrl: getOpenRouterAuthUrl(uriScheme), }, + { + slug: "vercel-ai-gateway", + name: "Vercel AI Gateway", + description: t("welcome:routers.vercelAiGateway.description"), + incentive: t("welcome:routers.vercelAiGateway.incentive"), + authUrl: + "https://vercel.com/d?to=%2F%5Bteam%5D%2F%7E%2Fai%2Fapi-keys&title=AI+Gateway+API+Key", + }, ] // Shuffle providers based on machine ID (will be consistent for the same machine) diff --git a/webview-ui/src/i18n/locales/en/welcome.json b/webview-ui/src/i18n/locales/en/welcome.json index 2202f6fd618b..56f3f2d6a074 100644 --- a/webview-ui/src/i18n/locales/en/welcome.json +++ b/webview-ui/src/i18n/locales/en/welcome.json @@ -10,6 +10,10 @@ }, "openrouter": { "description": "A unified interface for LLMs" + }, + "vercelAiGateway": { + "description": "The AI Gateway for Developers", + "incentive": "$5 free credits to use with any model" } }, "chooseProvider": "To do its magic, Roo needs an API key.", diff --git a/webview-ui/src/utils/__tests__/validate.test.ts b/webview-ui/src/utils/__tests__/validate.test.ts index f14fb8920048..30ccfd4463fd 100644 --- a/webview-ui/src/utils/__tests__/validate.test.ts +++ b/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 = { diff --git a/webview-ui/src/utils/validate.ts b/webview-ui/src/utils/validate.ts index 8f18e4411dfa..5613eb9eb8ee 100644 --- a/webview-ui/src/utils/validate.ts +++ b/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) { From 59b036ccfff5b8431bec99bba43084dd72c64920 Mon Sep 17 00:00:00 2001 From: joshualipman123 Date: Mon, 25 Aug 2025 12:18:16 -0700 Subject: [PATCH 02/13] add prompt caching support --- .../types/src/providers/vercel-ai-gateway.ts | 23 ++++++++++++++ src/api/providers/vercel-ai-gateway.ts | 7 +++-- .../transform/caching/vercel-ai-gateway.ts | 30 +++++++++++++++++++ webview-ui/src/i18n/locales/de/settings.json | 1 + webview-ui/src/i18n/locales/en/settings.json | 1 + webview-ui/src/i18n/locales/es/settings.json | 1 + webview-ui/src/i18n/locales/fr/settings.json | 1 + webview-ui/src/i18n/locales/it/settings.json | 1 + webview-ui/src/i18n/locales/ja/settings.json | 1 + webview-ui/src/i18n/locales/ko/settings.json | 1 + .../src/i18n/locales/pt-BR/settings.json | 1 + webview-ui/src/i18n/locales/ru/settings.json | 1 + .../src/i18n/locales/zh-CN/settings.json | 1 + .../src/i18n/locales/zh-TW/settings.json | 1 + 14 files changed, 68 insertions(+), 3 deletions(-) create mode 100644 src/api/transform/caching/vercel-ai-gateway.ts diff --git a/packages/types/src/providers/vercel-ai-gateway.ts b/packages/types/src/providers/vercel-ai-gateway.ts index 837baf7c520a..bded0b530dab 100644 --- a/packages/types/src/providers/vercel-ai-gateway.ts +++ b/packages/types/src/providers/vercel-ai-gateway.ts @@ -3,6 +3,29 @@ 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 vercelAiGatewayDefaultModelInfo: ModelInfo = { maxTokens: 64000, contextWindow: 128000, diff --git a/src/api/providers/vercel-ai-gateway.ts b/src/api/providers/vercel-ai-gateway.ts index 87fb7bab9c36..0b14d43c63bb 100644 --- a/src/api/providers/vercel-ai-gateway.ts +++ b/src/api/providers/vercel-ai-gateway.ts @@ -5,13 +5,14 @@ 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/anthropic" +import { addCacheBreakpoints } from "../transform/caching/vercel-ai-gateway" import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" import { RouterProvider } from "./router-provider" @@ -49,9 +50,9 @@ export class VercelAiGatewayHandler extends RouterProvider implements SingleComp ...convertToOpenAiMessages(messages), ] - if (modelId.startsWith("anthropic/claude-3")) { + if (VERCEL_AI_GATEWAY_PROMPT_CACHING_MODELS.has(modelId) && info.supportsPromptCache) { addCacheBreakpoints(systemPrompt, openAiMessages) - } //TODO: add cache breakpoints for other models + } const body: OpenAI.Chat.ChatCompletionCreateParams = { model: modelId, diff --git a/src/api/transform/caching/vercel-ai-gateway.ts b/src/api/transform/caching/vercel-ai-gateway.ts new file mode 100644 index 000000000000..82eff0cd7bf3 --- /dev/null +++ b/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" } + } + } + }) +} diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index 71006a9d4c1a..b9465d6c598e 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -233,6 +233,7 @@ "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", "doubaoApiKey": "Doubao API-Schlüssel", "getDoubaoApiKey": "Doubao API-Schlüssel erhalten", "apiKeyStorageNotice": "API-Schlüssel werden sicher im VSCode Secret Storage gespeichert", diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 6d761e85ffc2..85bf3109104a 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -232,6 +232,7 @@ "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", "apiKeyStorageNotice": "API keys are stored securely in VSCode's Secret Storage", "glamaApiKey": "Glama API Key", "getGlamaApiKey": "Get Glama API Key", diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index a3dbfa410c07..6eb3b5c3eb20 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -233,6 +233,7 @@ "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", "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", diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index a2c2d0ca0686..50124518d156 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -233,6 +233,7 @@ "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", "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", diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index 0cc2a590193c..66ec621813bb 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -233,6 +233,7 @@ "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", "apiKeyStorageNotice": "Le chiavi API sono memorizzate in modo sicuro nell'Archivio Segreto di VSCode", "glamaApiKey": "Chiave API Glama", "getGlamaApiKey": "Ottieni chiave API Glama", diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index 27b51854dd54..4e9b2848e20d 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -233,6 +233,7 @@ "awsCustomArnDesc": "ARN内のリージョンが上で選択したAWSリージョンと一致していることを確認してください。", "openRouterApiKey": "OpenRouter APIキー", "getOpenRouterApiKey": "OpenRouter APIキーを取得", + "vercelAiGatewayApiKey": "Vercel AI Gateway APIキー", "apiKeyStorageNotice": "APIキーはVSCodeのシークレットストレージに安全に保存されます", "glamaApiKey": "Glama APIキー", "getGlamaApiKey": "Glama APIキーを取得", diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index e6125f0ca839..2837b2dec3bd 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -233,6 +233,7 @@ "awsCustomArnDesc": "ARN의 리전이 위에서 선택한 AWS 리전과 일치하는지 확인하세요.", "openRouterApiKey": "OpenRouter API 키", "getOpenRouterApiKey": "OpenRouter API 키 받기", + "vercelAiGatewayApiKey": "Vercel AI Gateway API 키", "apiKeyStorageNotice": "API 키는 VSCode의 보안 저장소에 안전하게 저장됩니다", "glamaApiKey": "Glama API 키", "getGlamaApiKey": "Glama API 키 받기", diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index 7039497b9412..197c7af92242 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -233,6 +233,7 @@ "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", "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", diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index 05d73cd46f0c..a88dcfbc69c2 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -233,6 +233,7 @@ "awsCustomArnDesc": "Убедитесь, что регион в ARN совпадает с выбранным выше регионом AWS.", "openRouterApiKey": "OpenRouter API-ключ", "getOpenRouterApiKey": "Получить OpenRouter API-ключ", + "vercelAiGatewayApiKey": "Ключ API Vercel AI Gateway", "apiKeyStorageNotice": "API-ключи хранятся безопасно в Secret Storage VSCode", "glamaApiKey": "Glama API-ключ", "getGlamaApiKey": "Получить Glama API-ключ", diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index a7d30776846b..94b3db188538 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -233,6 +233,7 @@ "awsCustomArnDesc": "请确保ARN中的区域与上方选择的AWS区域一致。", "openRouterApiKey": "OpenRouter API 密钥", "getOpenRouterApiKey": "获取 OpenRouter API 密钥", + "vercelAiGatewayApiKey": "Vercel AI Gateway API 密钥", "apiKeyStorageNotice": "API 密钥安全存储在 VSCode 的密钥存储中", "glamaApiKey": "Glama API 密钥", "getGlamaApiKey": "获取 Glama API 密钥", diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index d8b71601dcba..31359291ce26 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -233,6 +233,7 @@ "awsCustomArnDesc": "確保 ARN 中的區域與您上面選擇的 AWS 區域相符。", "openRouterApiKey": "OpenRouter API 金鑰", "getOpenRouterApiKey": "取得 OpenRouter API 金鑰", + "vercelAiGatewayApiKey": "Vercel AI Gateway API 金鑰", "apiKeyStorageNotice": "API 金鑰安全儲存於 VSCode 金鑰儲存中", "glamaApiKey": "Glama API 金鑰", "getGlamaApiKey": "取得 Glama API 金鑰", From b8094b30a71c658fc9e1a1521b18644b2192d75b Mon Sep 17 00:00:00 2001 From: joshualipman123 Date: Mon, 25 Aug 2025 12:51:00 -0700 Subject: [PATCH 03/13] return cost 0 for free users --- src/api/providers/vercel-ai-gateway.ts | 2 ++ .../components/settings/providers/VercelAiGateway.tsx | 9 +++++++++ webview-ui/src/i18n/locales/de/settings.json | 1 + webview-ui/src/i18n/locales/en/settings.json | 3 ++- webview-ui/src/i18n/locales/es/settings.json | 1 + webview-ui/src/i18n/locales/fr/settings.json | 1 + webview-ui/src/i18n/locales/it/settings.json | 1 + webview-ui/src/i18n/locales/ja/settings.json | 1 + webview-ui/src/i18n/locales/ko/settings.json | 1 + webview-ui/src/i18n/locales/pt-BR/settings.json | 1 + webview-ui/src/i18n/locales/ru/settings.json | 1 + webview-ui/src/i18n/locales/zh-CN/settings.json | 1 + webview-ui/src/i18n/locales/zh-TW/settings.json | 1 + 13 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/api/providers/vercel-ai-gateway.ts b/src/api/providers/vercel-ai-gateway.ts index 0b14d43c63bb..093fffe26079 100644 --- a/src/api/providers/vercel-ai-gateway.ts +++ b/src/api/providers/vercel-ai-gateway.ts @@ -23,6 +23,7 @@ interface VercelAiGatewayUsage extends OpenAI.CompletionUsage { prompt_tokens_details?: { cached_tokens?: number } + cost?: number } export class VercelAiGatewayHandler extends RouterProvider implements SingleCompletionHandler { @@ -83,6 +84,7 @@ export class VercelAiGatewayHandler extends RouterProvider implements SingleComp outputTokens: usage.completion_tokens || 0, cacheWriteTokens: usage.cache_creation_input_tokens || undefined, cacheReadTokens: usage.prompt_tokens_details?.cached_tokens || undefined, + totalCost: usage.cost ?? 0, } } } diff --git a/webview-ui/src/components/settings/providers/VercelAiGateway.tsx b/webview-ui/src/components/settings/providers/VercelAiGateway.tsx index edbcc831892d..8050469efcb8 100644 --- a/webview-ui/src/components/settings/providers/VercelAiGateway.tsx +++ b/webview-ui/src/components/settings/providers/VercelAiGateway.tsx @@ -7,6 +7,7 @@ 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" @@ -52,6 +53,14 @@ export const VercelAiGateway = ({
{t("settings:providers.apiKeyStorageNotice")}
+ {!apiConfiguration?.vercelAiGatewayApiKey && ( + + {t("settings:providers.getVercelAiGatewayApiKey")} + + )} {{serviceName}}. If you're unsure which model to choose, Roo Code works best with {{defaultModelId}}. You can also try searching \"free\" for no-cost options currently available.", + "automaticFetch": "Note: Free tier users will see $0 costs as these requests are provided at no charge by Vercel AI Gateway.", "label": "Model", "searchPlaceholder": "Search", "noMatchFound": "No match found", diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index 6eb3b5c3eb20..da5a5c367d43 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -234,6 +234,7 @@ "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", diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index 50124518d156..6c73a7adfd4e 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -234,6 +234,7 @@ "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", diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index 66ec621813bb..e0cf5574d00a 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -234,6 +234,7 @@ "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", diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index 4e9b2848e20d..76661ec09193 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -234,6 +234,7 @@ "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キーを取得", diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index 2837b2dec3bd..463cad8d2634 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -234,6 +234,7 @@ "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 키 받기", diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index 197c7af92242..04655ba604e5 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -234,6 +234,7 @@ "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", diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index a88dcfbc69c2..c3b351b69be2 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -234,6 +234,7 @@ "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-ключ", diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index 94b3db188538..cb8dde46afa7 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -234,6 +234,7 @@ "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 密钥", diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index 31359291ce26..adf5d5fd95f6 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -234,6 +234,7 @@ "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 金鑰", From 5e873eee828d81891cc7b75ec0905f9a42197851 Mon Sep 17 00:00:00 2001 From: joshualipman123 Date: Mon, 25 Aug 2025 15:50:44 -0700 Subject: [PATCH 04/13] provide accurate model metadata --- .../types/src/providers/vercel-ai-gateway.ts | 60 +++++++++++++++++++ .../providers/fetchers/vercel-ai-gateway.ts | 11 +++- 2 files changed, 68 insertions(+), 3 deletions(-) diff --git a/packages/types/src/providers/vercel-ai-gateway.ts b/packages/types/src/providers/vercel-ai-gateway.ts index bded0b530dab..47f17ecd96f9 100644 --- a/packages/types/src/providers/vercel-ai-gateway.ts +++ b/packages/types/src/providers/vercel-ai-gateway.ts @@ -26,10 +26,70 @@ export const VERCEL_AI_GATEWAY_PROMPT_CACHING_MODELS = new Set([ "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: 128000, supportsImages: true, + supportsComputerUse: true, supportsPromptCache: true, inputPrice: 3, outputPrice: 15, diff --git a/src/api/providers/fetchers/vercel-ai-gateway.ts b/src/api/providers/fetchers/vercel-ai-gateway.ts index 4444a3ec40e4..91456819a61c 100644 --- a/src/api/providers/fetchers/vercel-ai-gateway.ts +++ b/src/api/providers/fetchers/vercel-ai-gateway.ts @@ -2,6 +2,7 @@ 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" @@ -67,8 +68,8 @@ export async function getVercelAiGatewayModels(options?: ApiHandlerOptions): Pro for (const model of data) { const { id } = model - // Filter out embedding models (models with "embedding" in name) - if (id.toLowerCase().includes("embed")) { + // Only include language models + if (model.type !== "language") { continue } @@ -98,11 +99,15 @@ export const parseVercelAiGatewayModel = ({ id, model }: { id: string; model: Ve 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: false, + supportsImages, + supportsComputerUse, supportsPromptCache, inputPrice: parseApiPrice(model.pricing?.input), outputPrice: parseApiPrice(model.pricing?.output), From 2705ed685369ce4ba4414dbd7da169a1de8ba92a Mon Sep 17 00:00:00 2001 From: joshualipman123 Date: Mon, 25 Aug 2025 16:46:12 -0700 Subject: [PATCH 05/13] fix missing translations --- packages/types/src/providers/vercel-ai-gateway.ts | 4 ++-- webview-ui/src/i18n/locales/ca/settings.json | 2 ++ webview-ui/src/i18n/locales/ca/welcome.json | 4 ++++ webview-ui/src/i18n/locales/de/welcome.json | 4 ++++ webview-ui/src/i18n/locales/en/settings.json | 2 +- webview-ui/src/i18n/locales/es/welcome.json | 4 ++++ webview-ui/src/i18n/locales/fr/welcome.json | 4 ++++ webview-ui/src/i18n/locales/hi/settings.json | 2 ++ webview-ui/src/i18n/locales/hi/welcome.json | 4 ++++ webview-ui/src/i18n/locales/id/settings.json | 2 ++ webview-ui/src/i18n/locales/id/welcome.json | 4 ++++ webview-ui/src/i18n/locales/it/welcome.json | 4 ++++ webview-ui/src/i18n/locales/ja/welcome.json | 4 ++++ webview-ui/src/i18n/locales/ko/welcome.json | 4 ++++ webview-ui/src/i18n/locales/nl/settings.json | 2 ++ webview-ui/src/i18n/locales/nl/welcome.json | 4 ++++ webview-ui/src/i18n/locales/pl/settings.json | 2 ++ webview-ui/src/i18n/locales/pl/welcome.json | 4 ++++ webview-ui/src/i18n/locales/pt-BR/welcome.json | 4 ++++ webview-ui/src/i18n/locales/ru/welcome.json | 4 ++++ webview-ui/src/i18n/locales/tr/settings.json | 2 ++ webview-ui/src/i18n/locales/tr/welcome.json | 4 ++++ webview-ui/src/i18n/locales/vi/settings.json | 2 ++ webview-ui/src/i18n/locales/vi/welcome.json | 4 ++++ webview-ui/src/i18n/locales/zh-CN/welcome.json | 4 ++++ webview-ui/src/i18n/locales/zh-TW/welcome.json | 4 ++++ 26 files changed, 85 insertions(+), 3 deletions(-) diff --git a/packages/types/src/providers/vercel-ai-gateway.ts b/packages/types/src/providers/vercel-ai-gateway.ts index 47f17ecd96f9..70cf49b41976 100644 --- a/packages/types/src/providers/vercel-ai-gateway.ts +++ b/packages/types/src/providers/vercel-ai-gateway.ts @@ -87,7 +87,7 @@ export const VERCEL_AI_GATEWAY_VISION_AND_TOOLS_MODELS = new Set([ export const vercelAiGatewayDefaultModelInfo: ModelInfo = { maxTokens: 64000, - contextWindow: 128000, + contextWindow: 200000, supportsImages: true, supportsComputerUse: true, supportsPromptCache: true, @@ -99,4 +99,4 @@ export const vercelAiGatewayDefaultModelInfo: ModelInfo = { "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 +export const VERCEL_AI_GATEWAY_DEFAULT_TEMPERATURE = 0.7 diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index 977ccaeec992..40209cc68ce3 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/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", diff --git a/webview-ui/src/i18n/locales/ca/welcome.json b/webview-ui/src/i18n/locales/ca/welcome.json index 8ef62ea205da..97a786632e6f 100644 --- a/webview-ui/src/i18n/locales/ca/welcome.json +++ b/webview-ui/src/i18n/locales/ca/welcome.json @@ -10,6 +10,10 @@ }, "openrouter": { "description": "Una interfície unificada per a LLMs" + }, + "vercelAiGateway": { + "description": "La passarel·la d'IA per a desenvolupadors", + "incentive": "$5 de crèdits gratuïts per utilitzar amb qualsevol model" } }, "chooseProvider": "Per fer la seva màgia, Roo necessita una clau API.", diff --git a/webview-ui/src/i18n/locales/de/welcome.json b/webview-ui/src/i18n/locales/de/welcome.json index 8b322380dff9..dc9944dd4f22 100644 --- a/webview-ui/src/i18n/locales/de/welcome.json +++ b/webview-ui/src/i18n/locales/de/welcome.json @@ -10,6 +10,10 @@ }, "openrouter": { "description": "Eine einheitliche Schnittstelle für LLMs" + }, + "vercelAiGateway": { + "description": "Das AI-Gateway für Entwickler", + "incentive": "$5 kostenlose Credits für die Nutzung mit jedem Modell" } }, "chooseProvider": "Um seine Magie zu entfalten, benötigt Roo einen API-Schlüssel.", diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index d93b1d2df6db..694a8be68fa9 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -760,7 +760,7 @@ } }, "modelPicker": { - "automaticFetch": "Note: Free tier users will see $0 costs as these requests are provided at no charge by Vercel AI Gateway.", + "automaticFetch": "The extension automatically fetches the latest list of models available on {{serviceName}}. If you're unsure which model to choose, Roo Code works best with {{defaultModelId}}. You can also try searching \"free\" for no-cost options currently available", "label": "Model", "searchPlaceholder": "Search", "noMatchFound": "No match found", diff --git a/webview-ui/src/i18n/locales/es/welcome.json b/webview-ui/src/i18n/locales/es/welcome.json index d4f3a20e08c2..df10daae25ae 100644 --- a/webview-ui/src/i18n/locales/es/welcome.json +++ b/webview-ui/src/i18n/locales/es/welcome.json @@ -10,6 +10,10 @@ }, "openrouter": { "description": "Una interfaz unificada para LLMs" + }, + "vercelAiGateway": { + "description": "La pasarela de IA para desarrolladores", + "incentive": "$5 de créditos gratuitos para usar con cualquier modelo" } }, "chooseProvider": "Para hacer su magia, Roo necesita una clave API.", diff --git a/webview-ui/src/i18n/locales/fr/welcome.json b/webview-ui/src/i18n/locales/fr/welcome.json index 2e1ead38cb03..78c3421463f2 100644 --- a/webview-ui/src/i18n/locales/fr/welcome.json +++ b/webview-ui/src/i18n/locales/fr/welcome.json @@ -10,6 +10,10 @@ }, "openrouter": { "description": "Une interface unifiée pour les LLMs" + }, + "vercelAiGateway": { + "description": "La passerelle IA pour les développeurs", + "incentive": "5 $ de crédits gratuits à utiliser avec n'importe quel modèle" } }, "chooseProvider": "Pour faire sa magie, Roo a besoin d'une clé API.", diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index 000b5f89b367..769091541204 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/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 कुंजी प्राप्त करें", diff --git a/webview-ui/src/i18n/locales/hi/welcome.json b/webview-ui/src/i18n/locales/hi/welcome.json index e8ef257f4862..ce96d6df3b8e 100644 --- a/webview-ui/src/i18n/locales/hi/welcome.json +++ b/webview-ui/src/i18n/locales/hi/welcome.json @@ -10,6 +10,10 @@ }, "openrouter": { "description": "LLMs के लिए एक एकीकृत इंटरफेस" + }, + "vercelAiGateway": { + "description": "डेवलपर्स के लिए AI गेटवे", + "incentive": "किसी भी मॉडल के साथ उपयोग के लिए $5 मुफ्त क्रेडिट" } }, "chooseProvider": "अपना जादू दिखाने के लिए, Roo को एक API कुंजी की आवश्यकता है।", diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index 0af73ecda9e4..83f3dd39be8f 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/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", diff --git a/webview-ui/src/i18n/locales/id/welcome.json b/webview-ui/src/i18n/locales/id/welcome.json index b1d6d71c80cd..b3fff69f78f5 100644 --- a/webview-ui/src/i18n/locales/id/welcome.json +++ b/webview-ui/src/i18n/locales/id/welcome.json @@ -10,6 +10,10 @@ }, "openrouter": { "description": "Interface terpadu untuk LLM" + }, + "vercelAiGateway": { + "description": "Gateway AI untuk Developer", + "incentive": "$5 kredit gratis untuk digunakan dengan model apa pun" } }, "chooseProvider": "Untuk melakukan keajaibannya, Roo membutuhkan API key.", diff --git a/webview-ui/src/i18n/locales/it/welcome.json b/webview-ui/src/i18n/locales/it/welcome.json index caa5f3e1d168..887997aff680 100644 --- a/webview-ui/src/i18n/locales/it/welcome.json +++ b/webview-ui/src/i18n/locales/it/welcome.json @@ -10,6 +10,10 @@ }, "openrouter": { "description": "Un'interfaccia unificata per LLMs" + }, + "vercelAiGateway": { + "description": "Il Gateway AI per sviluppatori", + "incentive": "$5 di crediti gratuiti da utilizzare con qualsiasi modello" } }, "chooseProvider": "Per fare la sua magia, Roo ha bisogno di una chiave API.", diff --git a/webview-ui/src/i18n/locales/ja/welcome.json b/webview-ui/src/i18n/locales/ja/welcome.json index bc4bad4918e3..a2feabac9e9b 100644 --- a/webview-ui/src/i18n/locales/ja/welcome.json +++ b/webview-ui/src/i18n/locales/ja/welcome.json @@ -10,6 +10,10 @@ }, "openrouter": { "description": "LLMsのための統一インターフェース" + }, + "vercelAiGateway": { + "description": "開発者向けAIゲートウェイ", + "incentive": "どのモデルでも使える5ドルの無料クレジット" } }, "chooseProvider": "Rooが機能するには、APIキーが必要です。", diff --git a/webview-ui/src/i18n/locales/ko/welcome.json b/webview-ui/src/i18n/locales/ko/welcome.json index 7637cf3d4286..5773b8652f00 100644 --- a/webview-ui/src/i18n/locales/ko/welcome.json +++ b/webview-ui/src/i18n/locales/ko/welcome.json @@ -10,6 +10,10 @@ }, "openrouter": { "description": "LLM을 위한 통합 인터페이스" + }, + "vercelAiGateway": { + "description": "개발자를 위한 AI 게이트웨이", + "incentive": "모든 모델에 사용할 수 있는 $5 무료 크레딧" } }, "chooseProvider": "Roo가 작동하려면 API 키가 필요합니다.", diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index ca2024522e6f..816ea37b9662 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/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", diff --git a/webview-ui/src/i18n/locales/nl/welcome.json b/webview-ui/src/i18n/locales/nl/welcome.json index c07d9c5ace82..6e1cae6a2c2b 100644 --- a/webview-ui/src/i18n/locales/nl/welcome.json +++ b/webview-ui/src/i18n/locales/nl/welcome.json @@ -10,6 +10,10 @@ }, "openrouter": { "description": "Een uniforme interface voor LLM's" + }, + "vercelAiGateway": { + "description": "De AI Gateway voor ontwikkelaars", + "incentive": "$5 gratis tegoed om te gebruiken met elk model" } }, "chooseProvider": "Om zijn magie te doen, heeft Roo een API-sleutel nodig.", diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index 25b04078bbca..2da116c79ede 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/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", diff --git a/webview-ui/src/i18n/locales/pl/welcome.json b/webview-ui/src/i18n/locales/pl/welcome.json index 5794dced84c7..f3e865a3ce87 100644 --- a/webview-ui/src/i18n/locales/pl/welcome.json +++ b/webview-ui/src/i18n/locales/pl/welcome.json @@ -10,6 +10,10 @@ }, "openrouter": { "description": "Ujednolicony interfejs dla LLMs" + }, + "vercelAiGateway": { + "description": "Brama AI dla deweloperów", + "incentive": "$5 darmowych środków do użycia z dowolnym modelem" } }, "chooseProvider": "Aby działać, Roo potrzebuje klucza API.", diff --git a/webview-ui/src/i18n/locales/pt-BR/welcome.json b/webview-ui/src/i18n/locales/pt-BR/welcome.json index 1c0dec8ca0a8..95fc46f7f26d 100644 --- a/webview-ui/src/i18n/locales/pt-BR/welcome.json +++ b/webview-ui/src/i18n/locales/pt-BR/welcome.json @@ -10,6 +10,10 @@ }, "openrouter": { "description": "Uma interface unificada para LLMs" + }, + "vercelAiGateway": { + "description": "O Gateway de IA para desenvolvedores", + "incentive": "$5 de créditos gratuitos para usar com qualquer modelo" } }, "chooseProvider": "Para fazer sua mágica, o Roo precisa de uma chave API.", diff --git a/webview-ui/src/i18n/locales/ru/welcome.json b/webview-ui/src/i18n/locales/ru/welcome.json index cae5b790e753..37f1d6596e08 100644 --- a/webview-ui/src/i18n/locales/ru/welcome.json +++ b/webview-ui/src/i18n/locales/ru/welcome.json @@ -10,6 +10,10 @@ }, "openrouter": { "description": "Унифицированный интерфейс для LLM" + }, + "vercelAiGateway": { + "description": "AI-шлюз для разработчиков", + "incentive": "$5 бесплатных кредитов для использования с любой моделью" } }, "chooseProvider": "Для своей магии Roo нуждается в API-ключе.", diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index 1bcaa7def571..3813b3db17eb 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/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", diff --git a/webview-ui/src/i18n/locales/tr/welcome.json b/webview-ui/src/i18n/locales/tr/welcome.json index 5f989fa75d65..be3e685545b2 100644 --- a/webview-ui/src/i18n/locales/tr/welcome.json +++ b/webview-ui/src/i18n/locales/tr/welcome.json @@ -10,6 +10,10 @@ }, "openrouter": { "description": "LLM'ler için birleşik bir arayüz" + }, + "vercelAiGateway": { + "description": "Geliştiriciler için AI Gateway", + "incentive": "Herhangi bir modelle kullanım için $5 ücretsiz kredi" } }, "chooseProvider": "Sihirini yapabilmesi için Roo'nun bir API anahtarına ihtiyacı var.", diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index b8cbb8c57179..3e1cfd7422f6 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/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", diff --git a/webview-ui/src/i18n/locales/vi/welcome.json b/webview-ui/src/i18n/locales/vi/welcome.json index 6eb484eb895e..49acdef96e72 100644 --- a/webview-ui/src/i18n/locales/vi/welcome.json +++ b/webview-ui/src/i18n/locales/vi/welcome.json @@ -10,6 +10,10 @@ }, "openrouter": { "description": "Giao diện thống nhất cho các LLM" + }, + "vercelAiGateway": { + "description": "Cổng AI dành cho nhà phát triển", + "incentive": "$5 tín dụng miễn phí để sử dụng với bất kỳ mô hình nào" } }, "chooseProvider": "Để thực hiện phép màu của mình, Roo cần một khóa API.", diff --git a/webview-ui/src/i18n/locales/zh-CN/welcome.json b/webview-ui/src/i18n/locales/zh-CN/welcome.json index 690a6aa19908..d180ce2622c7 100644 --- a/webview-ui/src/i18n/locales/zh-CN/welcome.json +++ b/webview-ui/src/i18n/locales/zh-CN/welcome.json @@ -10,6 +10,10 @@ }, "openrouter": { "description": "统一了大语言模型的接口" + }, + "vercelAiGateway": { + "description": "面向开发者的 AI 网关", + "incentive": "5美元免费额度,可用于任何模型" } }, "chooseProvider": "Roo 需要一个 API 密钥才能发挥魔力。", diff --git a/webview-ui/src/i18n/locales/zh-TW/welcome.json b/webview-ui/src/i18n/locales/zh-TW/welcome.json index 2384d4f3e91d..cb36c4520705 100644 --- a/webview-ui/src/i18n/locales/zh-TW/welcome.json +++ b/webview-ui/src/i18n/locales/zh-TW/welcome.json @@ -10,6 +10,10 @@ }, "openrouter": { "description": "LLM 的統一介面" + }, + "vercelAiGateway": { + "description": "面向開發者的 AI 閘道", + "incentive": "5美元免費額度,可用於任何模型" } }, "chooseProvider": "Roo 需要 API 金鑰才能發揮魔力。", From 51eb938beac6c80297bdb1a8f2865876b9aefb6a Mon Sep 17 00:00:00 2001 From: joshualipman123 Date: Mon, 25 Aug 2025 16:58:18 -0700 Subject: [PATCH 06/13] remove vercel ai gateway as a reccommended --- webview-ui/src/components/welcome/WelcomeView.tsx | 8 -------- webview-ui/src/i18n/locales/ca/welcome.json | 4 ---- webview-ui/src/i18n/locales/de/welcome.json | 4 ---- webview-ui/src/i18n/locales/en/welcome.json | 4 ---- webview-ui/src/i18n/locales/es/welcome.json | 4 ---- webview-ui/src/i18n/locales/fr/welcome.json | 4 ---- webview-ui/src/i18n/locales/hi/welcome.json | 4 ---- webview-ui/src/i18n/locales/id/welcome.json | 4 ---- webview-ui/src/i18n/locales/it/welcome.json | 4 ---- webview-ui/src/i18n/locales/ja/welcome.json | 4 ---- webview-ui/src/i18n/locales/ko/welcome.json | 4 ---- webview-ui/src/i18n/locales/nl/welcome.json | 4 ---- webview-ui/src/i18n/locales/pl/welcome.json | 4 ---- webview-ui/src/i18n/locales/pt-BR/welcome.json | 4 ---- webview-ui/src/i18n/locales/ru/welcome.json | 4 ---- webview-ui/src/i18n/locales/tr/welcome.json | 4 ---- webview-ui/src/i18n/locales/vi/welcome.json | 4 ---- webview-ui/src/i18n/locales/zh-CN/welcome.json | 4 ---- webview-ui/src/i18n/locales/zh-TW/welcome.json | 4 ---- 19 files changed, 80 deletions(-) diff --git a/webview-ui/src/components/welcome/WelcomeView.tsx b/webview-ui/src/components/welcome/WelcomeView.tsx index 40a1467e1216..215a290d0da9 100644 --- a/webview-ui/src/components/welcome/WelcomeView.tsx +++ b/webview-ui/src/components/welcome/WelcomeView.tsx @@ -83,14 +83,6 @@ const WelcomeView = () => { description: t("welcome:routers.openrouter.description"), authUrl: getOpenRouterAuthUrl(uriScheme), }, - { - slug: "vercel-ai-gateway", - name: "Vercel AI Gateway", - description: t("welcome:routers.vercelAiGateway.description"), - incentive: t("welcome:routers.vercelAiGateway.incentive"), - authUrl: - "https://vercel.com/d?to=%2F%5Bteam%5D%2F%7E%2Fai%2Fapi-keys&title=AI+Gateway+API+Key", - }, ] // Shuffle providers based on machine ID (will be consistent for the same machine) diff --git a/webview-ui/src/i18n/locales/ca/welcome.json b/webview-ui/src/i18n/locales/ca/welcome.json index 97a786632e6f..8ef62ea205da 100644 --- a/webview-ui/src/i18n/locales/ca/welcome.json +++ b/webview-ui/src/i18n/locales/ca/welcome.json @@ -10,10 +10,6 @@ }, "openrouter": { "description": "Una interfície unificada per a LLMs" - }, - "vercelAiGateway": { - "description": "La passarel·la d'IA per a desenvolupadors", - "incentive": "$5 de crèdits gratuïts per utilitzar amb qualsevol model" } }, "chooseProvider": "Per fer la seva màgia, Roo necessita una clau API.", diff --git a/webview-ui/src/i18n/locales/de/welcome.json b/webview-ui/src/i18n/locales/de/welcome.json index dc9944dd4f22..8b322380dff9 100644 --- a/webview-ui/src/i18n/locales/de/welcome.json +++ b/webview-ui/src/i18n/locales/de/welcome.json @@ -10,10 +10,6 @@ }, "openrouter": { "description": "Eine einheitliche Schnittstelle für LLMs" - }, - "vercelAiGateway": { - "description": "Das AI-Gateway für Entwickler", - "incentive": "$5 kostenlose Credits für die Nutzung mit jedem Modell" } }, "chooseProvider": "Um seine Magie zu entfalten, benötigt Roo einen API-Schlüssel.", diff --git a/webview-ui/src/i18n/locales/en/welcome.json b/webview-ui/src/i18n/locales/en/welcome.json index 56f3f2d6a074..2202f6fd618b 100644 --- a/webview-ui/src/i18n/locales/en/welcome.json +++ b/webview-ui/src/i18n/locales/en/welcome.json @@ -10,10 +10,6 @@ }, "openrouter": { "description": "A unified interface for LLMs" - }, - "vercelAiGateway": { - "description": "The AI Gateway for Developers", - "incentive": "$5 free credits to use with any model" } }, "chooseProvider": "To do its magic, Roo needs an API key.", diff --git a/webview-ui/src/i18n/locales/es/welcome.json b/webview-ui/src/i18n/locales/es/welcome.json index df10daae25ae..d4f3a20e08c2 100644 --- a/webview-ui/src/i18n/locales/es/welcome.json +++ b/webview-ui/src/i18n/locales/es/welcome.json @@ -10,10 +10,6 @@ }, "openrouter": { "description": "Una interfaz unificada para LLMs" - }, - "vercelAiGateway": { - "description": "La pasarela de IA para desarrolladores", - "incentive": "$5 de créditos gratuitos para usar con cualquier modelo" } }, "chooseProvider": "Para hacer su magia, Roo necesita una clave API.", diff --git a/webview-ui/src/i18n/locales/fr/welcome.json b/webview-ui/src/i18n/locales/fr/welcome.json index 78c3421463f2..2e1ead38cb03 100644 --- a/webview-ui/src/i18n/locales/fr/welcome.json +++ b/webview-ui/src/i18n/locales/fr/welcome.json @@ -10,10 +10,6 @@ }, "openrouter": { "description": "Une interface unifiée pour les LLMs" - }, - "vercelAiGateway": { - "description": "La passerelle IA pour les développeurs", - "incentive": "5 $ de crédits gratuits à utiliser avec n'importe quel modèle" } }, "chooseProvider": "Pour faire sa magie, Roo a besoin d'une clé API.", diff --git a/webview-ui/src/i18n/locales/hi/welcome.json b/webview-ui/src/i18n/locales/hi/welcome.json index ce96d6df3b8e..e8ef257f4862 100644 --- a/webview-ui/src/i18n/locales/hi/welcome.json +++ b/webview-ui/src/i18n/locales/hi/welcome.json @@ -10,10 +10,6 @@ }, "openrouter": { "description": "LLMs के लिए एक एकीकृत इंटरफेस" - }, - "vercelAiGateway": { - "description": "डेवलपर्स के लिए AI गेटवे", - "incentive": "किसी भी मॉडल के साथ उपयोग के लिए $5 मुफ्त क्रेडिट" } }, "chooseProvider": "अपना जादू दिखाने के लिए, Roo को एक API कुंजी की आवश्यकता है।", diff --git a/webview-ui/src/i18n/locales/id/welcome.json b/webview-ui/src/i18n/locales/id/welcome.json index b3fff69f78f5..b1d6d71c80cd 100644 --- a/webview-ui/src/i18n/locales/id/welcome.json +++ b/webview-ui/src/i18n/locales/id/welcome.json @@ -10,10 +10,6 @@ }, "openrouter": { "description": "Interface terpadu untuk LLM" - }, - "vercelAiGateway": { - "description": "Gateway AI untuk Developer", - "incentive": "$5 kredit gratis untuk digunakan dengan model apa pun" } }, "chooseProvider": "Untuk melakukan keajaibannya, Roo membutuhkan API key.", diff --git a/webview-ui/src/i18n/locales/it/welcome.json b/webview-ui/src/i18n/locales/it/welcome.json index 887997aff680..caa5f3e1d168 100644 --- a/webview-ui/src/i18n/locales/it/welcome.json +++ b/webview-ui/src/i18n/locales/it/welcome.json @@ -10,10 +10,6 @@ }, "openrouter": { "description": "Un'interfaccia unificata per LLMs" - }, - "vercelAiGateway": { - "description": "Il Gateway AI per sviluppatori", - "incentive": "$5 di crediti gratuiti da utilizzare con qualsiasi modello" } }, "chooseProvider": "Per fare la sua magia, Roo ha bisogno di una chiave API.", diff --git a/webview-ui/src/i18n/locales/ja/welcome.json b/webview-ui/src/i18n/locales/ja/welcome.json index a2feabac9e9b..bc4bad4918e3 100644 --- a/webview-ui/src/i18n/locales/ja/welcome.json +++ b/webview-ui/src/i18n/locales/ja/welcome.json @@ -10,10 +10,6 @@ }, "openrouter": { "description": "LLMsのための統一インターフェース" - }, - "vercelAiGateway": { - "description": "開発者向けAIゲートウェイ", - "incentive": "どのモデルでも使える5ドルの無料クレジット" } }, "chooseProvider": "Rooが機能するには、APIキーが必要です。", diff --git a/webview-ui/src/i18n/locales/ko/welcome.json b/webview-ui/src/i18n/locales/ko/welcome.json index 5773b8652f00..7637cf3d4286 100644 --- a/webview-ui/src/i18n/locales/ko/welcome.json +++ b/webview-ui/src/i18n/locales/ko/welcome.json @@ -10,10 +10,6 @@ }, "openrouter": { "description": "LLM을 위한 통합 인터페이스" - }, - "vercelAiGateway": { - "description": "개발자를 위한 AI 게이트웨이", - "incentive": "모든 모델에 사용할 수 있는 $5 무료 크레딧" } }, "chooseProvider": "Roo가 작동하려면 API 키가 필요합니다.", diff --git a/webview-ui/src/i18n/locales/nl/welcome.json b/webview-ui/src/i18n/locales/nl/welcome.json index 6e1cae6a2c2b..c07d9c5ace82 100644 --- a/webview-ui/src/i18n/locales/nl/welcome.json +++ b/webview-ui/src/i18n/locales/nl/welcome.json @@ -10,10 +10,6 @@ }, "openrouter": { "description": "Een uniforme interface voor LLM's" - }, - "vercelAiGateway": { - "description": "De AI Gateway voor ontwikkelaars", - "incentive": "$5 gratis tegoed om te gebruiken met elk model" } }, "chooseProvider": "Om zijn magie te doen, heeft Roo een API-sleutel nodig.", diff --git a/webview-ui/src/i18n/locales/pl/welcome.json b/webview-ui/src/i18n/locales/pl/welcome.json index f3e865a3ce87..5794dced84c7 100644 --- a/webview-ui/src/i18n/locales/pl/welcome.json +++ b/webview-ui/src/i18n/locales/pl/welcome.json @@ -10,10 +10,6 @@ }, "openrouter": { "description": "Ujednolicony interfejs dla LLMs" - }, - "vercelAiGateway": { - "description": "Brama AI dla deweloperów", - "incentive": "$5 darmowych środków do użycia z dowolnym modelem" } }, "chooseProvider": "Aby działać, Roo potrzebuje klucza API.", diff --git a/webview-ui/src/i18n/locales/pt-BR/welcome.json b/webview-ui/src/i18n/locales/pt-BR/welcome.json index 95fc46f7f26d..1c0dec8ca0a8 100644 --- a/webview-ui/src/i18n/locales/pt-BR/welcome.json +++ b/webview-ui/src/i18n/locales/pt-BR/welcome.json @@ -10,10 +10,6 @@ }, "openrouter": { "description": "Uma interface unificada para LLMs" - }, - "vercelAiGateway": { - "description": "O Gateway de IA para desenvolvedores", - "incentive": "$5 de créditos gratuitos para usar com qualquer modelo" } }, "chooseProvider": "Para fazer sua mágica, o Roo precisa de uma chave API.", diff --git a/webview-ui/src/i18n/locales/ru/welcome.json b/webview-ui/src/i18n/locales/ru/welcome.json index 37f1d6596e08..cae5b790e753 100644 --- a/webview-ui/src/i18n/locales/ru/welcome.json +++ b/webview-ui/src/i18n/locales/ru/welcome.json @@ -10,10 +10,6 @@ }, "openrouter": { "description": "Унифицированный интерфейс для LLM" - }, - "vercelAiGateway": { - "description": "AI-шлюз для разработчиков", - "incentive": "$5 бесплатных кредитов для использования с любой моделью" } }, "chooseProvider": "Для своей магии Roo нуждается в API-ключе.", diff --git a/webview-ui/src/i18n/locales/tr/welcome.json b/webview-ui/src/i18n/locales/tr/welcome.json index be3e685545b2..5f989fa75d65 100644 --- a/webview-ui/src/i18n/locales/tr/welcome.json +++ b/webview-ui/src/i18n/locales/tr/welcome.json @@ -10,10 +10,6 @@ }, "openrouter": { "description": "LLM'ler için birleşik bir arayüz" - }, - "vercelAiGateway": { - "description": "Geliştiriciler için AI Gateway", - "incentive": "Herhangi bir modelle kullanım için $5 ücretsiz kredi" } }, "chooseProvider": "Sihirini yapabilmesi için Roo'nun bir API anahtarına ihtiyacı var.", diff --git a/webview-ui/src/i18n/locales/vi/welcome.json b/webview-ui/src/i18n/locales/vi/welcome.json index 49acdef96e72..6eb484eb895e 100644 --- a/webview-ui/src/i18n/locales/vi/welcome.json +++ b/webview-ui/src/i18n/locales/vi/welcome.json @@ -10,10 +10,6 @@ }, "openrouter": { "description": "Giao diện thống nhất cho các LLM" - }, - "vercelAiGateway": { - "description": "Cổng AI dành cho nhà phát triển", - "incentive": "$5 tín dụng miễn phí để sử dụng với bất kỳ mô hình nào" } }, "chooseProvider": "Để thực hiện phép màu của mình, Roo cần một khóa API.", diff --git a/webview-ui/src/i18n/locales/zh-CN/welcome.json b/webview-ui/src/i18n/locales/zh-CN/welcome.json index d180ce2622c7..690a6aa19908 100644 --- a/webview-ui/src/i18n/locales/zh-CN/welcome.json +++ b/webview-ui/src/i18n/locales/zh-CN/welcome.json @@ -10,10 +10,6 @@ }, "openrouter": { "description": "统一了大语言模型的接口" - }, - "vercelAiGateway": { - "description": "面向开发者的 AI 网关", - "incentive": "5美元免费额度,可用于任何模型" } }, "chooseProvider": "Roo 需要一个 API 密钥才能发挥魔力。", diff --git a/webview-ui/src/i18n/locales/zh-TW/welcome.json b/webview-ui/src/i18n/locales/zh-TW/welcome.json index cb36c4520705..2384d4f3e91d 100644 --- a/webview-ui/src/i18n/locales/zh-TW/welcome.json +++ b/webview-ui/src/i18n/locales/zh-TW/welcome.json @@ -10,10 +10,6 @@ }, "openrouter": { "description": "LLM 的統一介面" - }, - "vercelAiGateway": { - "description": "面向開發者的 AI 閘道", - "incentive": "5美元免費額度,可用於任何模型" } }, "chooseProvider": "Roo 需要 API 金鑰才能發揮魔力。", From 6abba41abb998da21b5b8483509238d1916e6530 Mon Sep 17 00:00:00 2001 From: joshualipman123 Date: Mon, 25 Aug 2025 17:24:51 -0700 Subject: [PATCH 07/13] fix deprecated max_tokens --- src/api/providers/vercel-ai-gateway.ts | 7 ++----- src/assets/images/vercel-ai-gateway.png | Bin 875 -> 0 bytes 2 files changed, 2 insertions(+), 5 deletions(-) delete mode 100644 src/assets/images/vercel-ai-gateway.png diff --git a/src/api/providers/vercel-ai-gateway.ts b/src/api/providers/vercel-ai-gateway.ts index 093fffe26079..be77d35986b4 100644 --- a/src/api/providers/vercel-ai-gateway.ts +++ b/src/api/providers/vercel-ai-gateway.ts @@ -20,9 +20,6 @@ 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 - prompt_tokens_details?: { - cached_tokens?: number - } cost?: number } @@ -61,7 +58,7 @@ export class VercelAiGatewayHandler extends RouterProvider implements SingleComp temperature: this.supportsTemperature(modelId) ? (this.options.modelTemperature ?? VERCEL_AI_GATEWAY_DEFAULT_TEMPERATURE) : undefined, - max_tokens: info.maxTokens, + max_completion_tokens: info.maxTokens, stream: true, } @@ -104,7 +101,7 @@ export class VercelAiGatewayHandler extends RouterProvider implements SingleComp requestOptions.temperature = this.options.modelTemperature ?? VERCEL_AI_GATEWAY_DEFAULT_TEMPERATURE } - requestOptions.max_tokens = info.maxTokens + requestOptions.max_completion_tokens = info.maxTokens const response = await this.client.chat.completions.create(requestOptions) return response.choices[0]?.message.content || "" diff --git a/src/assets/images/vercel-ai-gateway.png b/src/assets/images/vercel-ai-gateway.png deleted file mode 100644 index 0c85df1fdecbfc4affad35e6cadf4664bbb0c98b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 875 zcmV-x1C;!UP)Z z88Aw^xE9+eO_s}U>EarU2R`6O*jHeRY<+o-;zyQCr)+&$<^VtOD=Y&nlpG;RH-2Tg z+yqjBJ49Gb7+EeYa#O-n1w1CKuvfrnsX?J>Bdjc!eNux$QV)DWMp!Q}LuOVuP9P)8 z<&4a%Fw6%AkQJ5%mPw5BO$xY%ysRwn0H_Q<(Jvd&k>#>E{6v2YKE2jT4^S6)r#JSZ zE6e3j=%GHC27E_fRue`a?!w0pSeqP9~^|W1_p-6MvspEP(^gtuRShA-U4Hi zib(oQVjD^qd;++?7+mr7hV?{u``Vv`km& Date: Tue, 26 Aug 2025 09:43:26 -0700 Subject: [PATCH 08/13] add tests --- .../__tests__/vercel-ai-gateway.spec.ts | 383 ++++++++++++++++++ .../__tests__/vercel-ai-gateway.spec.ts | 317 +++++++++++++++ .../__tests__/vercel-ai-gateway.spec.ts | 233 +++++++++++ 3 files changed, 933 insertions(+) create mode 100644 src/api/providers/__tests__/vercel-ai-gateway.spec.ts create mode 100644 src/api/providers/fetchers/__tests__/vercel-ai-gateway.spec.ts create mode 100644 src/api/transform/caching/__tests__/vercel-ai-gateway.spec.ts diff --git a/src/api/providers/__tests__/vercel-ai-gateway.spec.ts b/src/api/providers/__tests__/vercel-ai-gateway.spec.ts new file mode 100644 index 000000000000..a3e7c9e7d5b7 --- /dev/null +++ b/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, + }), + ) + }) + }) +}) diff --git a/src/api/providers/fetchers/__tests__/vercel-ai-gateway.spec.ts b/src/api/providers/fetchers/__tests__/vercel-ai-gateway.spec.ts new file mode 100644 index 000000000000..657d335b61b1 --- /dev/null +++ b/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) + } + }) + }) + }) +}) diff --git a/src/api/transform/caching/__tests__/vercel-ai-gateway.spec.ts b/src/api/transform/caching/__tests__/vercel-ai-gateway.spec.ts new file mode 100644 index 000000000000..86dc593f4f3f --- /dev/null +++ b/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" }, + }) + } + }) + }) +}) From d5ecce03f015062996065c13f8f7c9d583b9adeb Mon Sep 17 00:00:00 2001 From: joshualipman123 Date: Tue, 26 Aug 2025 11:10:47 -0700 Subject: [PATCH 09/13] revert automatic fetch message --- webview-ui/src/i18n/locales/en/settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 694a8be68fa9..c8cad691a8f7 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -760,7 +760,7 @@ } }, "modelPicker": { - "automaticFetch": "The extension automatically fetches the latest list of models available on {{serviceName}}. If you're unsure which model to choose, Roo Code works best with {{defaultModelId}}. You can also try searching \"free\" for no-cost options currently available", + "automaticFetch": "The extension automatically fetches the latest list of models available on {{serviceName}}. If you're unsure which model to choose, Roo Code works best with {{defaultModelId}}. You can also try searching \"free\" for no-cost options currently available.", "label": "Model", "searchPlaceholder": "Search", "noMatchFound": "No match found", From da5f42840e5df2a45cbdbbaab860aa53b1362be6 Mon Sep 17 00:00:00 2001 From: daniel-lxs Date: Tue, 26 Aug 2025 13:45:38 -0500 Subject: [PATCH 10/13] fix: Add vercel-ai-gateway to MODELS_BY_PROVIDER and dynamicProviders --- packages/types/src/provider-settings.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index ee301eaa6fb9..8941e4cdd836 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -550,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 = [ @@ -559,6 +560,7 @@ export const dynamicProviders = [ "openrouter", "requesty", "unbound", + "vercel-ai-gateway", ] as const satisfies readonly ProviderName[] export type DynamicProvider = (typeof dynamicProviders)[number] From 3a80cf5b08542cc4c57c97811d9c5b64d040c5d5 Mon Sep 17 00:00:00 2001 From: daniel-lxs Date: Tue, 26 Aug 2025 14:09:33 -0500 Subject: [PATCH 11/13] test: update ClineProvider tests to include vercel-ai-gateway provider --- src/core/webview/__tests__/ClineProvider.spec.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index 80c2f537a214..0d51890d8144 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/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, }, }) }) From 0c26394ec1f737df8e1f6d9c47bb51041a425846 Mon Sep 17 00:00:00 2001 From: daniel-lxs Date: Tue, 26 Aug 2025 14:38:04 -0500 Subject: [PATCH 12/13] fix: update tests to include vercel-ai-gateway provider expectations --- src/core/webview/__tests__/webviewMessageHandler.spec.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/core/webview/__tests__/webviewMessageHandler.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.spec.ts index 6f76974d89d3..06dbc0350251 100644 --- a/src/core/webview/__tests__/webviewMessageHandler.spec.ts +++ b/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, { From 1485cacc416a5f2b4b29bae63f0564558c11f430 Mon Sep 17 00:00:00 2001 From: cte Date: Tue, 26 Aug 2025 13:36:32 -0700 Subject: [PATCH 13/13] Update @roo-code/cloud --- packages/types/npm/package.metadata.json | 2 +- pnpm-lock.yaml | 18 +++++++++--------- src/package.json | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/types/npm/package.metadata.json b/packages/types/npm/package.metadata.json index 2b1aa701e251..374f4e62bfc4 100644 --- a/packages/types/npm/package.metadata.json +++ b/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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b683e48b6449..00788ad28e71 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -584,8 +584,8 @@ importers: specifier: ^1.14.0 version: 1.14.0(typescript@5.8.3) '@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/cloud@0.21.0': - resolution: {integrity: sha512-yNVybIjaS7Hy8GwDtGJc76N1WpCXGaCSlAEsW7VGjnojpxaIzV2GcJP1j1hg5q8HqLQnU4ixV0qXxOkxwhkEiA==} + '@roo-code/cloud@0.22.0': + resolution: {integrity: sha512-s1d4wcDYeDzcwr+YypMWDlNKL4f2osOZ3NoIlD36LCfFeMs+hnluZPS1oXX3WHtmPDC76vSzPMfwW2Ef41hEoA==} - '@roo-code/types@1.60.0': - resolution: {integrity: sha512-tQO6njPr/ZDNBoSHQg1/dpxfVEYeUzpKcernUxgJzmttn1zJbS0sc3CfUyPYOfYKB331z6O3KFUpaiqYFje1wA==} + '@roo-code/types@1.61.0': + resolution: {integrity: sha512-YJdFc6aYfaZ8EN08KbWaKLehRr1dcN3G3CzDjpppb08iehSEUZMycax/ryP5/G4vl34HTdtzyHNMboDen5ElUg==} '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} @@ -12563,9 +12563,9 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.40.2': optional: true - '@roo-code/cloud@0.21.0': + '@roo-code/cloud@0.22.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/types@1.60.0': + '@roo-code/types@1.61.0': dependencies: zod: 3.25.76 diff --git a/src/package.json b/src/package.json index 7b84e9759a63..1f5154968451 100644 --- a/src/package.json +++ b/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:^",