From 13af5992af78380f1b34ed943c8f2cef224fc249 Mon Sep 17 00:00:00 2001 From: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com> Date: Wed, 28 Aug 2024 05:35:49 -0400 Subject: [PATCH] Add GCP Vertex AI provider --- package-lock.json | 138 +++++++++++++++++++++-- package.json | 1 + src/api/index.ts | 3 + src/api/vertex.ts | 62 ++++++++++ src/providers/ClaudeDevProvider.ts | 12 ++ src/shared/api.ts | 45 +++++++- webview-ui/src/App.tsx | 3 +- webview-ui/src/components/ApiOptions.tsx | 55 +++++++++ webview-ui/src/utils/validate.ts | 5 + 9 files changed, 314 insertions(+), 10 deletions(-) create mode 100644 src/api/vertex.ts diff --git a/package-lock.json b/package-lock.json index 25862a6..52e9bec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,18 @@ { "name": "claude-dev", - "version": "1.4.15", + "version": "1.4.16", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "claude-dev", - "version": "1.4.15", + "version": "1.4.16", "license": "MIT", "dependencies": { "@anthropic-ai/bedrock-sdk": "^0.10.2", "@anthropic-ai/sdk": "^0.26.0", "@anthropic-ai/tokenizer": "^0.0.4", + "@anthropic-ai/vertex-sdk": "^0.4.1", "@types/clone-deep": "^4.0.4", "@vscode/codicons": "^0.0.36", "axios": "^1.7.4", @@ -106,6 +107,15 @@ "undici-types": "~5.26.4" } }, + "node_modules/@anthropic-ai/vertex-sdk": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@anthropic-ai/vertex-sdk/-/vertex-sdk-0.4.1.tgz", + "integrity": "sha512-RT/2CWzqyAcJDZWxnNc1mXa7XiiHDaQ9aknfW4mIDw6zE+Zj/R2vCKpTb0dIwrmHYNOyKQNaD7Z1ynDt9oXFWA==", + "dependencies": { + "@anthropic-ai/sdk": ">=0.14 <1", + "google-auth-library": "^9.4.2" + } + }, "node_modules/@aws-crypto/crc32": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", @@ -4867,7 +4877,6 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", - "dev": true, "license": "MIT", "dependencies": { "debug": "^4.3.4" @@ -5054,7 +5063,6 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, "funding": [ { "type": "github", @@ -5071,6 +5079,14 @@ ], "license": "MIT" }, + "node_modules/bignumber.js": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", + "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -5170,6 +5186,11 @@ "ieee754": "^1.2.1" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, "node_modules/c8": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/c8/-/c8-9.1.0.tgz", @@ -5516,7 +5537,6 @@ "version": "4.3.5", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", - "dev": true, "license": "MIT", "dependencies": { "ms": "2.1.2" @@ -5668,6 +5688,14 @@ "dev": true, "license": "MIT" }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", @@ -6125,6 +6153,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -6442,6 +6475,44 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gaxios": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gaxios/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gcp-metadata": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.0.tgz", + "integrity": "sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==", + "dependencies": { + "gaxios": "^6.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -6605,6 +6676,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/google-auth-library": { + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.14.0.tgz", + "integrity": "sha512-Y/eq+RWVs55Io/anIsm24sDS8X79Tq948zVLGaa7+KlJYYqaGwp1YI37w48nzrNi12RgnzMrQD4NzdmCowT90g==", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -6632,6 +6719,18 @@ "dev": true, "license": "MIT" }, + "node_modules/gtoken": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", + "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", + "dependencies": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/has-bigints": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", @@ -6762,7 +6861,6 @@ "version": "7.0.5", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", - "dev": true, "license": "MIT", "dependencies": { "agent-base": "^7.0.2", @@ -7366,6 +7464,14 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -7407,6 +7513,25 @@ "setimmediate": "^1.0.5" } }, + "node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -8949,7 +9074,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, "license": "MIT" }, "node_modules/safe-regex-test": { diff --git a/package.json b/package.json index 53af5ea..0cbc229 100644 --- a/package.json +++ b/package.json @@ -135,6 +135,7 @@ "@anthropic-ai/bedrock-sdk": "^0.10.2", "@anthropic-ai/sdk": "^0.26.0", "@anthropic-ai/tokenizer": "^0.0.4", + "@anthropic-ai/vertex-sdk": "^0.4.1", "@types/clone-deep": "^4.0.4", "@vscode/codicons": "^0.0.36", "axios": "^1.7.4", diff --git a/src/api/index.ts b/src/api/index.ts index 4c725ae..bbafc1f 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -3,6 +3,7 @@ import { ApiConfiguration, ApiModelId, ModelInfo } from "../shared/api" import { AnthropicHandler } from "./anthropic" import { AwsBedrockHandler } from "./bedrock" import { OpenRouterHandler } from "./openrouter" +import { VertexHandler } from "./vertex" export interface ApiHandlerMessageResponse { message: Anthropic.Messages.Message @@ -37,6 +38,8 @@ export function buildApiHandler(configuration: ApiConfiguration): ApiHandler { return new OpenRouterHandler(options) case "bedrock": return new AwsBedrockHandler(options) + case "vertex": + return new VertexHandler(options) default: return new AnthropicHandler(options) } diff --git a/src/api/vertex.ts b/src/api/vertex.ts new file mode 100644 index 0000000..b9199bb --- /dev/null +++ b/src/api/vertex.ts @@ -0,0 +1,62 @@ +import { AnthropicVertex } from "@anthropic-ai/vertex-sdk" +import { Anthropic } from "@anthropic-ai/sdk" +import { ApiHandler, ApiHandlerMessageResponse, withoutImageData } from "." +import { ApiHandlerOptions, ModelInfo, vertexDefaultModelId, VertexModelId, vertexModels } from "../shared/api" + +// https://docs.anthropic.com/en/api/claude-on-vertex-ai +export class VertexHandler implements ApiHandler { + private options: ApiHandlerOptions + private client: AnthropicVertex + + constructor(options: ApiHandlerOptions) { + this.options = options + this.client = new AnthropicVertex({ + projectId: this.options.vertexProjectId, + // https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/use-claude#regions + region: this.options.vertexRegion, + }) + } + + async createMessage( + systemPrompt: string, + messages: Anthropic.Messages.MessageParam[], + tools: Anthropic.Messages.Tool[] + ): Promise { + const message = await this.client.messages.create({ + model: this.getModel().id, + max_tokens: this.getModel().info.maxTokens, + system: systemPrompt, + messages, + tools, + tool_choice: { type: "auto" }, + }) + return { message } + } + + createUserReadableRequest( + userContent: Array< + | Anthropic.TextBlockParam + | Anthropic.ImageBlockParam + | Anthropic.ToolUseBlockParam + | Anthropic.ToolResultBlockParam + > + ): any { + return { + model: this.getModel().id, + max_tokens: this.getModel().info.maxTokens, + system: "(see SYSTEM_PROMPT in src/ClaudeDev.ts)", + messages: [{ conversation_history: "..." }, { role: "user", content: withoutImageData(userContent) }], + tools: "(see tools in src/ClaudeDev.ts)", + tool_choice: { type: "auto" }, + } + } + + getModel(): { id: VertexModelId; info: ModelInfo } { + const modelId = this.options.apiModelId + if (modelId && modelId in vertexModels) { + const id = modelId as VertexModelId + return { id, info: vertexModels[id] } + } + return { id: vertexDefaultModelId, info: vertexModels[vertexDefaultModelId] } + } +} diff --git a/src/providers/ClaudeDevProvider.ts b/src/providers/ClaudeDevProvider.ts index 1bc6f19..cbe472a 100644 --- a/src/providers/ClaudeDevProvider.ts +++ b/src/providers/ClaudeDevProvider.ts @@ -20,6 +20,8 @@ type GlobalStateKey = | "apiProvider" | "apiModelId" | "awsRegion" + | "vertexProjectId" + | "vertexRegion" | "maxRequestsPerTask" | "lastShownAnnouncementId" | "customInstructions" @@ -311,6 +313,8 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider { awsAccessKey, awsSecretKey, awsRegion, + vertexProjectId, + vertexRegion, } = message.apiConfiguration await this.updateGlobalState("apiProvider", apiProvider) await this.updateGlobalState("apiModelId", apiModelId) @@ -319,6 +323,8 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider { await this.storeSecret("awsAccessKey", awsAccessKey) await this.storeSecret("awsSecretKey", awsSecretKey) await this.updateGlobalState("awsRegion", awsRegion) + await this.updateGlobalState("vertexProjectId", vertexProjectId) + await this.updateGlobalState("vertexRegion", vertexRegion) this.claudeDev?.updateApi(message.apiConfiguration) } await this.postStateToWebview() @@ -598,6 +604,8 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider { awsAccessKey, awsSecretKey, awsRegion, + vertexProjectId, + vertexRegion, maxRequestsPerTask, lastShownAnnouncementId, customInstructions, @@ -611,6 +619,8 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider { this.getSecret("awsAccessKey") as Promise, this.getSecret("awsSecretKey") as Promise, this.getGlobalState("awsRegion") as Promise, + this.getGlobalState("vertexProjectId") as Promise, + this.getGlobalState("vertexRegion") as Promise, this.getGlobalState("maxRequestsPerTask") as Promise, this.getGlobalState("lastShownAnnouncementId") as Promise, this.getGlobalState("customInstructions") as Promise, @@ -641,6 +651,8 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider { awsAccessKey, awsSecretKey, awsRegion, + vertexProjectId, + vertexRegion, }, maxRequestsPerTask, lastShownAnnouncementId, diff --git a/src/shared/api.ts b/src/shared/api.ts index b4fb300..22694f3 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -1,4 +1,4 @@ -export type ApiProvider = "anthropic" | "openrouter" | "bedrock" +export type ApiProvider = "anthropic" | "openrouter" | "bedrock" | "vertex" export interface ApiHandlerOptions { apiModelId?: ApiModelId @@ -7,6 +7,8 @@ export interface ApiHandlerOptions { awsAccessKey?: string awsSecretKey?: string awsRegion?: string + vertexProjectId?: string + vertexRegion?: string } export type ApiConfiguration = ApiHandlerOptions & { @@ -26,7 +28,7 @@ export interface ModelInfo { cacheReadsPrice?: number } -export type ApiModelId = AnthropicModelId | OpenRouterModelId | BedrockModelId +export type ApiModelId = AnthropicModelId | OpenRouterModelId | BedrockModelId | VertexModelId // Anthropic // https://docs.anthropic.com/en/docs/about-claude/models @@ -250,3 +252,42 @@ export const openRouterModels = { // outputPrice: 1.5, // }, } as const satisfies Record + +// Vertex AI +// https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/use-claude +export type VertexModelId = keyof typeof vertexModels +export const vertexDefaultModelId: VertexModelId = "claude-3-5-sonnet@20240620" +export const vertexModels = { + "claude-3-5-sonnet@20240620": { + maxTokens: 8192, + contextWindow: 200_000, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 3.0, + outputPrice: 15.0, + }, + "claude-3-opus@20240229": { + maxTokens: 4096, + contextWindow: 200_000, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 15.0, + outputPrice: 75.0, + }, + "claude-3-sonnet@20240229": { + maxTokens: 4096, + contextWindow: 200_000, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 3.0, + outputPrice: 15.0, + }, + "claude-3-haiku@20240307": { + maxTokens: 4096, + contextWindow: 200_000, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 0.25, + outputPrice: 1.25, + }, +} as const satisfies Record diff --git a/webview-ui/src/App.tsx b/webview-ui/src/App.tsx index 5acf863..e3eb02f 100644 --- a/webview-ui/src/App.tsx +++ b/webview-ui/src/App.tsx @@ -23,7 +23,8 @@ const AppContent = () => { const hasKey = message.state!.apiConfiguration?.apiKey !== undefined || message.state!.apiConfiguration?.openRouterApiKey !== undefined || - message.state!.apiConfiguration?.awsAccessKey !== undefined + message.state!.apiConfiguration?.awsAccessKey !== undefined || + message.state!.apiConfiguration?.vertexProjectId !== undefined setShowWelcome(!hasKey) // don't update showAnnouncement to false if shouldShowAnnouncement is false if (message.state!.shouldShowAnnouncement) { diff --git a/webview-ui/src/components/ApiOptions.tsx b/webview-ui/src/components/ApiOptions.tsx index f925169..c03a18b 100644 --- a/webview-ui/src/components/ApiOptions.tsx +++ b/webview-ui/src/components/ApiOptions.tsx @@ -10,6 +10,8 @@ import { bedrockModels, openRouterDefaultModelId, openRouterModels, + vertexDefaultModelId, + vertexModels, } from "../../../src/shared/api" import { useExtensionState } from "../context/ExtensionStateContext" @@ -70,6 +72,7 @@ const ApiOptions: React.FC = ({ showModelOptions, apiErrorMessa Anthropic AWS Bedrock OpenRouter + GCP Vertex AI @@ -188,6 +191,55 @@ const ApiOptions: React.FC = ({ showModelOptions, apiErrorMessa )} + {apiConfiguration?.apiProvider === "vertex" && ( +
+ + Google Cloud Project ID + +
+ + + Select a region... + us-east5 + us-central1 + europe-west1 + europe-west4 + asia-southeast1 + +
+

+ To use Google Cloud Vertex AI, you need to + + { + "1) create a Google Cloud account > enable the Vertex AI API > enable the desired Claude models," + } + {" "} + + {"2) install the Google Cloud CLI > configure Application Default Credentials."} + +

+
+ )} + {apiErrorMessage && (

= ({ showModelOptions, apiErrorMessa {selectedProvider === "anthropic" && createDropdown(anthropicModels)} {selectedProvider === "openrouter" && createDropdown(openRouterModels)} {selectedProvider === "bedrock" && createDropdown(bedrockModels)} + {selectedProvider === "vertex" && createDropdown(vertexModels)} @@ -311,6 +364,8 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration) { return getProviderData(openRouterModels, openRouterDefaultModelId) case "bedrock": return getProviderData(bedrockModels, bedrockDefaultModelId) + case "vertex": + return getProviderData(vertexModels, vertexDefaultModelId) default: return getProviderData(anthropicModels, anthropicDefaultModelId) } diff --git a/webview-ui/src/utils/validate.ts b/webview-ui/src/utils/validate.ts index 288ebd1..2bbfc94 100644 --- a/webview-ui/src/utils/validate.ts +++ b/webview-ui/src/utils/validate.ts @@ -18,6 +18,11 @@ export function validateApiConfiguration(apiConfiguration?: ApiConfiguration): s return "You must provide a valid API key or choose a different provider." } break + case "vertex": + if (!apiConfiguration.vertexProjectId || !apiConfiguration.vertexRegion) { + return "You must provide a valid Google Cloud Project ID and Region." + } + break } } return undefined